diff --git a/README.md b/README.md new file mode 100644 index 00000000..19f8f618 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Vera +Because UI should be simple. + +Vera is a simple yet powerful Fabric UI library. There are currently no plans for a Forge version.\ +[Submit a Logo](https://github.com/snackbag/vera/issues/new) [Visit Wiki](https://wiki.snackbag.net/w/vera) + +## Features + +- Various standard widgets + - Labels + - Checkboxes + - Dropdowns + - Images + - Text input + - Tabs + - Rectangles + - Easy creation of custom widgets +- Styling system written from the ground up +- Customizable animation system + - Style-composite rendering pipeline + - (Custom) easings! +- Layout system +- HUD-rendering +- 2D rendering developer QOL +- App hierarchy +- Simple keybindings +- Heavy optimization +- (Developer) QOL with [Verto](https://github.com/snackbag/verto) +- Extensive documentation + +### Coming soon + +- Full docstrings everywhere +- More standard composites +- Multi-versions +- More precise font options +- Vertex & fragment shaders +- Revised rendering API +- Double buffer rendering +- Vanilla-UI abilities +- Widget Compounds \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 23fb2960..254fb614 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ yarn_mappings=1.20.1+build.10 loader_version=0.16.7 # Mod Properties -mod_version=1.9.2 +mod_version=2.0.0 maven_group=net.snackbag.mcvera archives_base_name=mcvera diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..df97d72b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index f5feea6d..faf93008 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -206,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. diff --git a/src/main/java/com/example/demo/DemoApplication.java b/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 00000000..f7c5fa89 --- /dev/null +++ b/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,64 @@ +package com.example.demo; + +import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VCursorShape; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.VShortcut; +import net.snackbag.vera.flag.VLayoutAlignmentFlag; +import net.snackbag.vera.layout.VHLayout; +import net.snackbag.vera.layout.VVLayout; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.widget.VLabel; + +public class DemoApplication extends VeraApp { + private int clicks = 0; + private VHLayout layout; + private VVLayout centerLayout; + + @Override + public void init() { + new VShortcut(this, "escape", this::hide); + setBackgroundColor(VColor.black().withOpacity(0.2f)); + + layout = new VHLayout(this, 0, 0); + layout.alignment = VLayoutAlignmentFlag.CENTER; + + centerLayout = new VVLayout(layout); + centerLayout.alignment = VLayoutAlignmentFlag.CENTER; + + VLabel label = new VLabel("Not clicked yet", this).alsoAddTo(centerLayout); + label.modifyFontColor().rgb(VColor.white()); + + VLabel button = new VLabel("Click me", this).alsoAddTo(centerLayout); + + button.modifyFontColor().rgb(VColor.of(95, 180, 0)); + button.setStyle("background-color", VColor.white()); + button.setStyle("padding", 4); + button.setStyle("overlay", VStyleState.HOVERED, VColor.white().withOpacity(0.5f)); + button.setStyle("border-size", 1); + button.setStyle("border-color", VColor.of(95, 180, 0)); + button.setStyle("cursor", VCursorShape.POINTING_HAND); + + button.onLeftClick(() -> { + clicks++; + label.setText("Clicks: " + clicks); + label.adjustSize(); + }); + + new VShortcut(this, "leftctrl+d", () -> { + System.out.println("Debug hit"); + }); + } + + @Override + public void update() { + super.update(); + + setWidth(Vera.provider.getScreenWidth()); + setHeight(Vera.provider.getScreenHeight()); + + layout.setSize(getWidth(), getHeight()); + centerLayout.setHeight(getHeight()); + } +} diff --git a/src/main/java/com/example/demo/DemoMod.java b/src/main/java/com/example/demo/DemoMod.java new file mode 100644 index 00000000..3330c9bd --- /dev/null +++ b/src/main/java/com/example/demo/DemoMod.java @@ -0,0 +1,7 @@ +package com.example.demo; + +public class DemoMod { + public static void init() { + new DemoApplication().show(); + } +} diff --git a/src/main/java/net/snackbag/mcvera/InternalCommands.java b/src/main/java/net/snackbag/mcvera/InternalCommands.java new file mode 100644 index 00000000..a6b921e5 --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/InternalCommands.java @@ -0,0 +1,63 @@ +package net.snackbag.mcvera; + +import com.example.demo.DemoMod; +import com.mojang.brigadier.CommandDispatcher; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.command.CommandRegistryAccess; +import net.snackbag.mcvera.test.*; + +public class InternalCommands { + public static void register( + CommandDispatcher dispatcher, + CommandRegistryAccess ra + ) { + if (!FabricLoader.getInstance().isDevelopmentEnvironment()) return; + + dispatcher.register( + ClientCommandManager.literal("vera") + .then(ClientCommandManager.literal("test") + .then(ClientCommandManager.literal("generic").executes((ctx) -> { + TestApplication.INSTANCE.show(); + return 1; + })) + .then(ClientCommandManager.literal("styles").executes((ctx) -> { + StyleTestApplication.INSTANCE.show(); + return 1; + })) + .then(ClientCommandManager.literal("layout").executes((ctx) -> { + LayoutTestApplication.INSTANCE.show(); + return 1; + })) + .then(ClientCommandManager.literal("layoutalignments").executes((ctx) -> { + LayoutCenteringTestApplication.INSTANCE.show(); + return 1; + })) + .then(ClientCommandManager.literal("hierarchy").executes(ctx -> { + HierarchyTest.INSTANCE.start(); + return 1; + })) + .then(ClientCommandManager.literal("quads").executes(ctx -> { + QuadTestApplication.INSTANCE.show(); + return 1; + })) + .then(ClientCommandManager.literal("demo").executes(ctx -> { + DemoMod.init(); + return 1; + })) + ) + .then(ClientCommandManager.literal("clear-tests") + .executes((ctx) -> { + TestApplication.INSTANCE = new TestApplication(); + StyleTestApplication.INSTANCE = new StyleTestApplication(); + LayoutTestApplication.INSTANCE = new LayoutTestApplication(); + LayoutCenteringTestApplication.INSTANCE = new LayoutCenteringTestApplication(); + HierarchyTest.INSTANCE = new HierarchyTest(); + QuadTestApplication.INSTANCE = new QuadTestApplication(); + return 1; + }) + ) + ); + } +} diff --git a/src/main/java/net/snackbag/mcvera/MCVeraData.java b/src/main/java/net/snackbag/mcvera/MCVeraData.java index 663b6299..906e744d 100644 --- a/src/main/java/net/snackbag/mcvera/MCVeraData.java +++ b/src/main/java/net/snackbag/mcvera/MCVeraData.java @@ -1,17 +1,56 @@ package net.snackbag.mcvera; import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.flag.VAppFlag; +import net.snackbag.vera.flag.VAppPositioningFlag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; +import java.util.function.Consumer; public class MCVeraData { - public static Set applications = new HashSet<>(); - public static Set visibleApplications = new HashSet<>(); + public static LinkedHashSet applications = new LinkedHashSet<>(); + public static HashMap> visibleApplications = new HashMap<>(); + public static HashMap> appFlags = new HashMap<>(); + + public static int appsWithMouseRequired = 0; + public static final List pressedKeys = new ArrayList<>(); public static List previousPressedKeys = new ArrayList<>(); - public static final Set debugApps = new HashSet<>(); - public static int appsWithMouseRequired = 0; + + /** + * Executes a method as the top hierarchy app. If there is no top app it won't execute the specified code. + * + * @param runnable code to execute + * @return whether something has been executed + */ + public static boolean asTopHierarchy(@NotNull Consumer runnable) { + if (!appFlags.containsKey(VAppFlag.HIERARCHIC)) return false; + runnable.accept(getTopHierarchy()); + return true; + } + + public static @Nullable VeraApp getTopHierarchy() { + List apps = getAppsWithFlag(VAppFlag.HIERARCHIC); + if (apps.isEmpty()) return null; + return apps.get(0); + } + + public static boolean isTopHierarchy(VeraApp app) { + return getTopHierarchy() == app; + } + + /** + * Returns an UNMODIFIABLE version of the {@link #appFlags} entry for the given flag. If the entry is empty, it + * returns an empty unmodifiable list. If you want to access a modifiable version of the flag, you have to manually + * work with the {@link #appFlags} variable. + * + * @param flag the flag to check + * @return an unmodifiable list of the apps under the flag + */ + public static List getAppsWithFlag(VAppFlag flag) { + if (!appFlags.containsKey(flag)) return Collections.unmodifiableList(new ArrayList<>()); + return Collections.unmodifiableList(appFlags.get(flag)); + } } diff --git a/src/main/java/net/snackbag/mcvera/MinecraftVera.java b/src/main/java/net/snackbag/mcvera/MinecraftVera.java index 6a4262f1..8a4d1ed8 100644 --- a/src/main/java/net/snackbag/mcvera/MinecraftVera.java +++ b/src/main/java/net/snackbag/mcvera/MinecraftVera.java @@ -2,6 +2,8 @@ import net.fabricmc.api.ModInitializer; +import net.snackbag.vera.Vera; +import net.snackbag.vera.style.standard.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,5 +14,16 @@ public class MinecraftVera implements ModInitializer { @Override public void onInitialize() { LOGGER.info("Loading Vera..."); + + LOGGER.info("Registering standard styles"); + // sorted by "complicatedness" & importance + Vera.registrar.registerStandardStyle(new WidgetStandardStyle()); + + Vera.registrar.registerStandardStyle(new RectStandardStyle()); + Vera.registrar.registerStandardStyle(new CheckBoxStandardStyle()); + Vera.registrar.registerStandardStyle(new LabelStandardStyle()); + Vera.registrar.registerStandardStyle(new DropdownStandardStyle()); + Vera.registrar.registerStandardStyle(new TabWidgetStandardStyle()); + Vera.registrar.registerStandardStyle(new LineInputStandardStyle()); } } \ No newline at end of file diff --git a/src/main/java/net/snackbag/mcvera/MinecraftVeraClient.java b/src/main/java/net/snackbag/mcvera/MinecraftVeraClient.java index 67f02eeb..d3fad0d8 100644 --- a/src/main/java/net/snackbag/mcvera/MinecraftVeraClient.java +++ b/src/main/java/net/snackbag/mcvera/MinecraftVeraClient.java @@ -4,16 +4,12 @@ import com.google.gson.JsonObject; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; -import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; import net.minecraft.client.MinecraftClient; import net.minecraft.client.util.InputUtil; import net.minecraft.resource.Resource; import net.minecraft.util.Identifier; -import net.snackbag.mcvera.impl.MCVeraProvider; -import net.snackbag.mcvera.impl.MCVeraRenderer; import net.snackbag.mcvera.test.TestHandler; import net.snackbag.vera.Vera; -import net.snackbag.vera.core.VeraApp; import java.io.IOException; import java.io.InputStreamReader; @@ -25,24 +21,13 @@ public void onInitializeClient() { MinecraftVera.LOGGER.info("Loading client Vera implementation..."); TestHandler.impl(false); - HudRenderCallback.EVENT.register((context, tickDelta) -> { - MCVeraRenderer.drawContext = context; - MCVeraRenderer renderer = MCVeraRenderer.getInstance(); - - for (VeraApp app : MCVeraData.visibleApplications) { - renderer.renderApp(app); - } - }); - ClientTickEvents.END_CLIENT_TICK.register((client) -> { // only when changing if (MCVeraData.previousPressedKeys.equals(MCVeraData.pressedKeys)) return; MCVeraData.previousPressedKeys = new ArrayList<>(MCVeraData.pressedKeys); String combination = makeCombination(client); - for (VeraApp app : MCVeraData.visibleApplications) { - app.handleShortcut(combination); - } + Vera.forVisibleAndAllowedApps(app -> app.handleShortcut(combination)); }); } diff --git a/src/main/java/net/snackbag/mcvera/impl/MCVeraProvider.java b/src/main/java/net/snackbag/mcvera/impl/MCVeraProvider.java index f448dd94..f7af9299 100644 --- a/src/main/java/net/snackbag/mcvera/impl/MCVeraProvider.java +++ b/src/main/java/net/snackbag/mcvera/impl/MCVeraProvider.java @@ -1,35 +1,33 @@ package net.snackbag.mcvera.impl; import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; import net.snackbag.mcvera.MCVeraData; import net.snackbag.mcvera.screen.VeraVisibilityScreen; import net.snackbag.vera.Vera; import net.snackbag.vera.core.VFont; import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.VEvents; import net.snackbag.vera.event.VShortcut; +import net.snackbag.vera.flag.VAppFlag; import net.snackbag.vera.widget.VWidget; import java.nio.file.Path; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; public class MCVeraProvider { public void handleAppInitialization(VeraApp app) { MCVeraData.applications.add(app); + Vera.registrar.applyStandardWidgetStyles(app.styleSheet); MinecraftClient.getInstance().send(app::init); MinecraftClient.getInstance().send(app::update); - - app.addShortcut(new VShortcut(app, "LeftCtrl+LeftAlt+LeftShift+D", () -> { - MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.of("Debug mode enabled")); - MCVeraData.debugApps.add(app); - }, false)); } public void handleAppShow(VeraApp app) { if (app.isVisible()) return; - MCVeraData.visibleApplications.add(app); - if (app.isMouseRequired()) MCVeraData.appsWithMouseRequired += 1; + MCVeraData.visibleApplications.get(app.getPositioning()).add(app); + if (app.hasFlag(VAppFlag.REQUIRES_MOUSE)) MCVeraData.appsWithMouseRequired += 1; MinecraftClient client = MinecraftClient.getInstance(); client.send(app::update); @@ -43,8 +41,8 @@ public void handleAppShow(VeraApp app) { public void handleAppHide(VeraApp app) { if (!app.isVisible()) return; - if (app.isMouseRequired()) MCVeraData.appsWithMouseRequired -= 1; - MCVeraData.visibleApplications.remove(app); + if (app.hasFlag(VAppFlag.REQUIRES_MOUSE)) MCVeraData.appsWithMouseRequired -= 1; + MCVeraData.visibleApplications.get(app.getPositioning()).remove(app); MinecraftClient client = MinecraftClient.getInstance(); client.send(app::update); @@ -101,15 +99,11 @@ public int getMouseY() { } public void handleKeyPressed(int keyCode, int scanCode, int modifiers) { - for (VeraApp app : MCVeraData.visibleApplications) { - app.keyPressed(keyCode, scanCode, modifiers); - } + Vera.forVisibleAndAllowedApps(app -> app.keyPressed(keyCode, scanCode, modifiers)); } public void handleCharTyped(char chr, int modifiers) { - for (VeraApp app : MCVeraData.visibleApplications) { - app.charTyped(chr, modifiers); - } + Vera.forVisibleAndAllowedApps(app -> app.charTyped(chr, modifiers)); } public String getDefaultFontName() { @@ -129,8 +123,29 @@ public void handleAppSetMouseRequired(VeraApp app, boolean mouseRequired) { } public void handleFilesDropped(List paths) { - Vera.forHoveredWidget(Vera.getMouseX(), Vera.getMouseY(), (widget) -> { - widget.fireEvent("files-dropped", paths); + VeraApp top = MCVeraData.getTopHierarchy(); + + int x = Vera.getMouseX(); + int y = Vera.getMouseY(); + + if (top != null && top.isPointOverThis(x, y)) { + VWidget widget = top.getTopWidgetAt(x, y); + if (widget != null) { + widget.events.fire(VEvents.Widget.FILES_DROPPED, paths); + return; + } + } + + AtomicBoolean didSomething = new AtomicBoolean(false); + Vera.forAllVisibleApps(app -> { + if (didSomething.get()) return; + if (!app.isPointOverThis(x, y)) return; + + VWidget widget = app.getTopWidgetAt(x, y); + if (widget != null) { + widget.events.fire(VEvents.Widget.FILES_DROPPED, paths); + didSomething.set(true); + } }); } } diff --git a/src/main/java/net/snackbag/mcvera/impl/MCVeraRegistrar.java b/src/main/java/net/snackbag/mcvera/impl/MCVeraRegistrar.java new file mode 100644 index 00000000..ca915cf2 --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/impl/MCVeraRegistrar.java @@ -0,0 +1,39 @@ +package net.snackbag.mcvera.impl; + +import net.snackbag.vera.style.animation.easing.VEasing; +import net.snackbag.vera.style.standard.VStandardStyle; +import net.snackbag.vera.style.VStyleSheet; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Main Vera registry manager. Use with caution: there are (almost) no + * safety checks. Therefore, it is recommended to use the classes that directly + * implement registrar functionality than touching it yourself. + */ +public class MCVeraRegistrar { + private final List standardStyles = new ArrayList<>(); + private final HashMap easings = new HashMap<>(); + + public void registerStandardStyle(VStandardStyle style) { + standardStyles.add(style); + } + + public void applyStandardWidgetStyles(VStyleSheet sheet) { + for (VStandardStyle standardStyle : standardStyles) { + standardStyle.reserve(sheet); + standardStyle.apply(sheet); + } + } + + public void registerEasing(String name, VEasing easing) { + easings.put(name.toLowerCase(), easing); + } + + public @Nullable VEasing getEasingIgnoreCase(String name) { + return easings.getOrDefault(name.toLowerCase(), null); + } +} diff --git a/src/main/java/net/snackbag/mcvera/impl/MCVeraRenderer.java b/src/main/java/net/snackbag/mcvera/impl/MCVeraRenderer.java index c72b978f..04e32839 100644 --- a/src/main/java/net/snackbag/mcvera/impl/MCVeraRenderer.java +++ b/src/main/java/net/snackbag/mcvera/impl/MCVeraRenderer.java @@ -3,83 +3,437 @@ import com.mojang.blaze3d.systems.RenderSystem; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.render.*; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Style; import net.minecraft.text.Text; import net.minecraft.util.Identifier; import net.minecraft.util.math.RotationAxis; -import net.snackbag.vera.core.VColor; -import net.snackbag.vera.core.VFont; -import net.snackbag.vera.core.VeraApp; +import net.snackbag.mcvera.MCVeraData; +import net.snackbag.mcvera.mixin.DrawContextAccessor; +import net.snackbag.vera.Vera; +import net.snackbag.vera.core.*; +import net.snackbag.vera.flag.VAppFlag; +import net.snackbag.vera.flag.VAppPositioningFlag; import net.snackbag.vera.widget.VWidget; +import org.joml.Matrix4f; +import org.lwjgl.opengl.GL11; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; public class MCVeraRenderer { - private static MCVeraRenderer instance = null; public static DrawContext drawContext = null; - public static MCVeraRenderer getInstance() { - if (instance == null) { - instance = new MCVeraRenderer(); - } + // + // Widget rendering + // + + public void pushContext(VRenderContext ctx) { + MatrixStack stack = drawContext.getMatrices(); + stack.push(); - return instance; + float wMod = (ctx.width / 2f) * (ctx.scale - 1); + float hMod = (ctx.height / 2f) * (ctx.scale - 1); + + float xRot = ctx.x + ctx.width / 2f; + float yRot = ctx.y + ctx.height / 2f; + + // Rotation + stack.translate(xRot, yRot, 0f); + stack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(ctx.rotation)); + stack.translate(-xRot, -yRot, 0f); + + // Scale & final positioning) + stack.translate(ctx.x - wMod, ctx.y - hMod, 0f); + stack.scale(ctx.scale, ctx.scale, 1.0f); } - public void drawRect(VeraApp app, int x, int y, int width, int height, double rotation, VColor color) { + public void popContext() { + drawContext.getMatrices().pop(); + } + + public void drawRect(VRenderContext ctx, int x, int y, int width, int height, VColor color) { + renderColQuad( + x, y, + x, y + height, + x + width, y + height, + x + width, y, color + ); + } + + public void drawText(VRenderContext ctx, int x, int y, String text, VFont font) { MatrixStack stack = drawContext.getMatrices(); stack.push(); - int centerX = x + width / 2; - int centerY = y + height / 2; + drawText( + x, y, + text, font + ); + stack.pop(); + } - stack.translate(app.getX(), app.getY(), 0); - stack.translate(centerX, centerY, 0); - stack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees((float) rotation)); - stack.translate(-width / 2, -height / 2, 0); + public void drawImage(VRenderContext ctx, int x, int y, int width, int height, Identifier path) { + renderTexQuad( + ctx.hasTransparency, path, + x, y, + x, y + height, + x + width, y + height, + x + width, y + ); + } - drawContext.fill(0, 0, width, height, color.toInt()); + public void drawFill(VRenderContext ctx, int x, int y, int width, int height, VFill fill) { + fill.renderQuad(ctx, x, y, width, height); + } - stack.pop(); + // + // Basic rendering + // + + public void drawRect(int x, int y, int width, int height, VColor color) { + drawContext.fill(x, y, x + width, y + height, color.toIntArgb()); } - public void drawText(VeraApp app, int x, int y, double rotation, String text, VFont font) { + public void drawText(int x, int y, String text, VFont font) { float scaleFactor = font.getSize() / 16.0f; drawContext.getMatrices().push(); - drawContext.getMatrices().translate(x + app.getX(), y + app.getY(), 0); + drawContext.getMatrices().translate(x, y, 0); drawContext.getMatrices().scale(scaleFactor, scaleFactor, 1.0f); drawContext.drawText( MinecraftClient.getInstance().textRenderer, Text.literal(text).setStyle(Style.EMPTY.withFont(new Identifier(font.getName()))), - 0, 0, // x and y are handled by translate - font.getColor().toInt(), + 0, 0, + font.getColor().toIntArgb(), false ); drawContext.getMatrices().pop(); } - public void drawImage(VeraApp app, int x, int y, int width, int height, double rotation, Identifier path) { - drawContext.drawTexture(path, x + app.getX(), y + app.getY(), 0, 0, width, height, width, height); + public void drawImage(int x, int y, int width, int height, Identifier path) { + drawContext.drawTexture(path, x, y, 0, 0, width, height, width, height); + } + + // + // Low-level rendering + // + + /** + * Renders a solid-colored quad to the GUI render layer. + * + *

Vertices must be provided in counter-clockwise order in screen space + * (Minecraft GUI coordinates, where Y increases downward):

+ * + *
+     * v1 ── v4
+     * │     │
+     * v2 ── v3
+     * 
+ * + *
    + *
  • v1 = top-left
  • + *
  • v2 = bottom-left
  • + *
  • v3 = bottom-right
  • + *
  • v4 = top-right
  • + *
+ * + *

No validation or reordering is performed. Incorrect vertex order or + * duplicated vertices will result in visual artifacts or no output.

+ * + *

All vertices are rendered with the same color.

+ * + *

This method renders using {@code RenderLayer.getGui()} and immediately + * flushes the vertex buffer.

+ * + * @param v1x top-left x + * @param v1y top-left y + * @param v2x bottom-left x + * @param v2y bottom-left y + * @param v3x bottom-right x + * @param v3y bottom-right y + * @param v4x top-right x + * @param v4y top-right y + * @param color color applied to all vertices + */ + public void renderColQuad( + int v1x, int v1y, + int v2x, int v2y, + int v3x, int v3y, + int v4x, int v4y, + VColor color + ) { + renderColQuad(v1x, v1y, color, v2x, v2y, color, v3x, v3y, color, v4x, v4y, color); + } + + /** + * Renders a quad to the GUI render layer with per-vertex colors. + * + *

Vertices must be provided in counter-clockwise order in screen space + * (Minecraft GUI coordinates, where Y increases downward):

+ * + *
+     * v1 ── v4
+     * │     │
+     * v2 ── v3
+     * 
+ * + *

No validation or reordering is performed.

+ * + *

This method renders using {@code RenderLayer.getGui()} and immediately + * flushes the vertex buffer.

+ * + * @param v1x top-left x + * @param v1y top-left y + * @param v1col color at v1 + * @param v2x bottom-left x + * @param v2y bottom-left y + * @param v2col color at v2 + * @param v3x bottom-right x + * @param v3y bottom-right y + * @param v3col color at v3 + * @param v4x top-right x + * @param v4y top-right y + * @param v4col color at v4 + */ + public void renderColQuad( + int v1x, int v1y, VColor v1col, + int v2x, int v2y, VColor v2col, + int v3x, int v3y, VColor v3col, + int v4x, int v4y, VColor v4col + ) { + Matrix4f matrix = drawContext.getMatrices().peek().getPositionMatrix(); + + VertexConsumer consumer = drawContext.getVertexConsumers().getBuffer(RenderLayer.getGui()); + consumer.vertex(matrix, (float) v1x, (float) v1y, 0f).color(v1col.toIntArgb()).next(); + consumer.vertex(matrix, (float) v2x, (float) v2y, 0f).color(v2col.toIntArgb()).next(); + consumer.vertex(matrix, (float) v3x, (float) v3y, 0f).color(v3col.toIntArgb()).next(); + consumer.vertex(matrix, (float) v4x, (float) v4y, 0f).color(v4col.toIntArgb()).next(); + + ((DrawContextAccessor) drawContext).vera$invokeTryDraw(); + } + + /** + * Renders a textured quad to the GUI render layer using the full texture. + * + *

The texture is automatically bound via the provided + * {@link net.minecraft.util.Identifier}.

+ * + *

Blending is automatically enabled and disabled based on + * {@code hasTransparentParts}.

+ * + *

Vertices must be provided in counter-clockwise order in screen space + * (Minecraft GUI coordinates, where Y increases downward):

+ * + *
+     * v1 ── v4
+     * │     │
+     * v2 ── v3
+     * 
+ * + *

Texture coordinates are automatically mapped to the full texture + * (u,v in the range 0.0–1.0).

+ * + *

The vertex buffer is flushed immediately.

+ * + * @param hasTransparentParts whether the texture has transparent parts; handles blending + * @param texture texture identifier to bind + * @param v1x top-left x + * @param v1y top-left y + * @param v2x bottom-left x + * @param v2y bottom-left y + * @param v3x bottom-right x + * @param v3y bottom-right y + * @param v4x top-right x + * @param v4y top-right y + */ + public void renderTexQuad( + boolean hasTransparentParts, + Identifier texture, + int v1x, int v1y, + int v2x, int v2y, + int v3x, int v3y, + int v4x, int v4y + ) { + VColor color = VColor.white(); + + renderTexQuad( + hasTransparentParts, texture, color, + v1x, v1y, v2x, v2y, v3x, v3y, v4x, v4y + ); + } + + /** + * Renders a textured quad to the GUI render layer using the full texture. + * + *

The texture is automatically bound via the provided + * {@link net.minecraft.util.Identifier}.

+ * + *

Blending is automatically enabled and disabled based on + * {@code hasTransparentParts}.

+ * + *

Vertices must be provided in counter-clockwise order in screen space + * (Minecraft GUI coordinates, where Y increases downward):

+ * + *
+     * v1 ── v4
+     * │     │
+     * v2 ── v3
+     * 
+ * + *

Texture coordinates are automatically mapped to the full texture + * (u,v in the range 0.0–1.0).

+ * + *

The vertex buffer is flushed immediately.

+ * + * @param hasTransparentParts whether the texture has transparent parts; handles blending + * @param texture texture identifier to bind + * @param color tint to render with + * @param v1x top-left x + * @param v1y top-left y + * @param v2x bottom-left x + * @param v2y bottom-left y + * @param v3x bottom-right x + * @param v3y bottom-right y + * @param v4x top-right x + * @param v4y top-right y + */ + public void renderTexQuad( + boolean hasTransparentParts, + Identifier texture, + VColor color, + int v1x, int v1y, + int v2x, int v2y, + int v3x, int v3y, + int v4x, int v4y + ) { + renderTexQuad( + hasTransparentParts, texture, + v1x, v1y, 0.0f, 0.0f, color, + v2x, v2y, 0.0f, 1.0f, color, + v3x, v3y, 1.0f, 1.0f, color, + v4x, v4y, 1.0f, 0.0f, color + ); + } + + /** + * Renders a textured quad to the GUI render layer with per-vertex UVs. + * + *

The texture is automatically bound via the provided + * {@link net.minecraft.util.Identifier}.

+ * + *

Blending is automatically enabled and disabled based on + * {@code hasTransparentParts}.

+ * + *

Vertices must be provided in counter-clockwise order in screen space + * (Minecraft GUI coordinates, where Y increases downward).

+ * + *

No validation or UV normalization is performed.

+ * + *

The vertex buffer is flushed immediately.

+ * + * @param hasTransparentParts whether the texture has transparent parts; handles blending + * @param texture texture identifier to bind + * @param v1x top-left x + * @param v1y top-left y + * @param u1 texture u at v1 + * @param v1t texture v at v1 + * @param v1c tint at v1 + * @param v2x bottom-left x + * @param v2y bottom-left y + * @param u2 texture u at v2 + * @param v2t texture v at v2 + * @param v2c tint at v2 + * @param v3x bottom-right x + * @param v3y bottom-right y + * @param u3 texture u at v3 + * @param v3t texture v at v3 + * @param v3c tint at v3 + * @param v4x top-right x + * @param v4y top-right y + * @param u4 texture u at v4 + * @param v4t texture v at v4 + * @param v4c tint at v4 + */ + public void renderTexQuad( + boolean hasTransparentParts, + Identifier texture, + int v1x, int v1y, float u1, float v1t, VColor v1c, + int v2x, int v2y, float u2, float v2t, VColor v2c, + int v3x, int v3y, float u3, float v3t, VColor v3c, + int v4x, int v4y, float u4, float v4t, VColor v4c + ) { + RenderSystem.setShaderTexture(0, texture); + RenderSystem.setShader(GameRenderer::getPositionColorTexProgram); + if (hasTransparentParts) RenderSystem.enableBlend(); + + Matrix4f matrix = drawContext.getMatrices().peek().getPositionMatrix(); + + BufferBuilder buf = Tessellator.getInstance().getBuffer(); + buf.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE); + buf.vertex(matrix, v1x, v1y, 0f).color(v1c.toIntArgb()).texture(u1, v1t).next(); + buf.vertex(matrix, v2x, v2y, 0f).color(v2c.toIntArgb()).texture(u2, v2t).next(); + buf.vertex(matrix, v3x, v3y, 0f).color(v3c.toIntArgb()).texture(u3, v3t).next(); + buf.vertex(matrix, v4x, v4y, 0f).color(v4c.toIntArgb()).texture(u4, v4t).next(); + + BufferRenderer.drawWithGlobalProgram(buf.end()); + if (hasTransparentParts) RenderSystem.disableBlend(); + } + + // + // Apps + // + + public void ensureClearContext(VRenderContext ctx) { + if (ctx.peekClip() != null) { + throw new RuntimeException("Unclosed clip stack"); + } } public void renderApp(VeraApp app) { + boolean blendEnabled = GL11.glIsEnabled(GL11.GL_BLEND); + List> widgets = app.getWidgets(); - RenderSystem.enableBlend(); + if (!blendEnabled) RenderSystem.enableBlend(); app.render(); - List> hoveredWidgets = app.getHoveredWidgets(); + VWidget hoveredWidget = !app.hasFlag(VAppFlag.HIERARCHIC) || MCVeraData.isTopHierarchy(app) + ? app.getTopWidgetAt(Vera.getMouseX(), Vera.getMouseY()) + : null; + for (VWidget widget : widgets) { - widget.setHovered(hoveredWidgets.contains(widget)); - if (widget.visibilityConditionsPassed()) { - widget.render(); - widget.renderBorder(); - } + if (widget != hoveredWidget && widget.isHovered()) widget.setHovered(false); + else if (widget == hoveredWidget && !widget.isHovered()) widget.setHovered(true); + + widget.renderSelf(); } app.renderAfterWidgets(); - RenderSystem.disableBlend(); + if (app.hasFlag(VAppFlag.HIERARCHIC) && !MCVeraData.isTopHierarchy(app)) app.renderHierarchyOverlay(); + + if (!blendEnabled) RenderSystem.disableBlend(); + } + + public void renderApps(VAppPositioningFlag flag) { + if (!MinecraftClient.getInstance().isRunning()) return; + + LinkedHashSet apps = MCVeraData.visibleApplications.getOrDefault(flag, new LinkedHashSet<>()); + + for (VeraApp app : apps) { + if (app.hasFlag(VAppFlag.HIERARCHIC)) continue; + Vera.renderer.renderApp(app); + } + + List hierarchicApps = new ArrayList<>(MCVeraData.getAppsWithFlag(VAppFlag.HIERARCHIC)); + Collections.reverse(hierarchicApps); + for (VeraApp app : hierarchicApps) { + if (app.getPositioning() != flag || !MCVeraData.visibleApplications.get(app.getPositioning()).contains(app)) { + continue; + } + Vera.renderer.renderApp(app); + } } } diff --git a/src/main/java/net/snackbag/mcvera/mixin/DrawContextAccessor.java b/src/main/java/net/snackbag/mcvera/mixin/DrawContextAccessor.java new file mode 100644 index 00000000..e3f2befd --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/mixin/DrawContextAccessor.java @@ -0,0 +1,11 @@ +package net.snackbag.mcvera.mixin; + +import net.minecraft.client.gui.DrawContext; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(DrawContext.class) +public interface DrawContextAccessor { + @Invoker("tryDraw") + void vera$invokeTryDraw(); +} diff --git a/src/main/java/net/snackbag/mcvera/mixin/DrawContextMixin.java b/src/main/java/net/snackbag/mcvera/mixin/DrawContextMixin.java new file mode 100644 index 00000000..63bb2476 --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/mixin/DrawContextMixin.java @@ -0,0 +1,22 @@ +package net.snackbag.mcvera.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.util.math.MatrixStack; +import net.snackbag.mcvera.impl.MCVeraRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(DrawContext.class) +@Environment(EnvType.CLIENT) +public abstract class DrawContextMixin { + @Inject(at = @At("TAIL"), method = "(Lnet/minecraft/client/MinecraftClient;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider$Immediate;)V") + private void snackbag$updateVeraDrawContext(MinecraftClient client, MatrixStack matrices, VertexConsumerProvider.Immediate vertexConsumers, CallbackInfo ci) { + MCVeraRenderer.drawContext = (DrawContext) ((Object) this); + } +} diff --git a/src/main/java/net/snackbag/mcvera/mixin/GameRendererMixin.java b/src/main/java/net/snackbag/mcvera/mixin/GameRendererMixin.java new file mode 100644 index 00000000..e373d140 --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/mixin/GameRendererMixin.java @@ -0,0 +1,42 @@ +package net.snackbag.mcvera.mixin; + +import net.minecraft.client.render.GameRenderer; +import net.snackbag.vera.InternalVera; +import net.snackbag.vera.Vera; +import net.snackbag.vera.flag.VAppPositioningFlag; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.List; + +@Mixin(GameRenderer.class) +public abstract class GameRendererMixin { + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;getOverlay()Lnet/minecraft/client/gui/screen/Overlay;", ordinal = 0, shift = At.Shift.BEFORE)) + private void mcvera$renderAboveHud(float tickDelta, long startTime, boolean tick, CallbackInfo ci) { + Vera.renderer.renderApps(VAppPositioningFlag.ABOVE_HUD); + } + + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;renderWithTooltip(Lnet/minecraft/client/gui/DrawContext;IIF)V")) + private void mcvera$renderOnGUI(float tickDelta, long startTime, boolean tick, CallbackInfo ci) { + Vera.renderer.renderApps(VAppPositioningFlag.GUI); + } + + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;renderWithTooltip(Lnet/minecraft/client/gui/DrawContext;IIF)V", shift = At.Shift.AFTER)) + private void mcvera$renderAboveGUI(float tickDelta, long startTime, boolean tick, CallbackInfo ci) { + Vera.renderer.renderApps(VAppPositioningFlag.ABOVE_GUI); + } + + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/toast/ToastManager;draw(Lnet/minecraft/client/gui/DrawContext;)V", shift = At.Shift.AFTER)) + private void mcvera$renderScreenAndTop(float tickDelta, long startTime, boolean tick, CallbackInfo ci) { + Vera.renderer.renderApps(VAppPositioningFlag.SCREEN); + Vera.renderer.renderApps(VAppPositioningFlag.TOP); + + List tasks = new ArrayList<>(InternalVera.getScheduledTasks()); + InternalVera.clearScheduledTasks(); + + for (Runnable task : tasks) task.run(); + } +} diff --git a/src/main/java/net/snackbag/mcvera/mixin/InGameHudMixin.java b/src/main/java/net/snackbag/mcvera/mixin/InGameHudMixin.java new file mode 100644 index 00000000..836bb4bc --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/mixin/InGameHudMixin.java @@ -0,0 +1,38 @@ +package net.snackbag.mcvera.mixin; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.hud.InGameHud; +import net.snackbag.vera.Vera; +import net.snackbag.vera.flag.VAppPositioningFlag; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(InGameHud.class) +public abstract class InGameHudMixin { + @Inject(at = @At(value = "HEAD"), method = "render") + private void mcvera$beginRender(DrawContext context, float tickDelta, CallbackInfo ci) { + Vera.renderCacheId = System.currentTimeMillis(); + } + + @Inject(at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;enableBlend()V", shift = At.Shift.AFTER, ordinal = 0, remap = false), method = "render") + private void mcvera$renderBelowVignette(DrawContext context, float tickDelta, CallbackInfo ci) { + Vera.renderer.renderApps(VAppPositioningFlag.BELOW_VIGNETTE); + } + + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;getLastFrameDuration()F"), method = "render") + private void mcvera$renderBelowOverlays(DrawContext context, float tickDelta, CallbackInfo ci) { + Vera.renderer.renderApps(VAppPositioningFlag.BELOW_OVERLAYS); + } + + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;getCurrentGameMode()Lnet/minecraft/world/GameMode;", ordinal = 0, shift = At.Shift.BEFORE), method = "render") + private void mcvera$renderBelowHud(DrawContext context, float tickDelta, CallbackInfo ci) { + Vera.renderer.renderApps(VAppPositioningFlag.BELOW_HUD); + } + + @Inject(at = @At(value = "TAIL"), method = "renderHotbar") + private void mcvera$renderHud(float tickDelta, DrawContext context, CallbackInfo ci) { + Vera.renderer.renderApps(VAppPositioningFlag.HUD); + } +} diff --git a/src/main/java/net/snackbag/mcvera/mixin/MinecraftClientMixin.java b/src/main/java/net/snackbag/mcvera/mixin/MinecraftClientMixin.java index 540b98e6..cbcbf65d 100644 --- a/src/main/java/net/snackbag/mcvera/mixin/MinecraftClientMixin.java +++ b/src/main/java/net/snackbag/mcvera/mixin/MinecraftClientMixin.java @@ -4,7 +4,7 @@ import net.minecraft.client.gui.screen.Screen; import net.snackbag.mcvera.MCVeraData; import net.snackbag.mcvera.screen.VeraVisibilityScreen; -import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.Vera; import net.snackbag.vera.widget.VWidget; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; @@ -36,11 +36,11 @@ public abstract class MinecraftClientMixin { private void mcvera$handleResize(CallbackInfo ci) { MinecraftClient client = MinecraftClient.getInstance(); - for (VeraApp app : MCVeraData.visibleApplications) { + Vera.forAllVisibleApps(app -> { client.send(app::update); for (VWidget widget : app.getWidgets()) { client.send(widget::update); } - } + }); } } \ No newline at end of file diff --git a/src/main/java/net/snackbag/mcvera/mixin/MouseMixin.java b/src/main/java/net/snackbag/mcvera/mixin/MouseMixin.java index 4f486493..cbe1c15f 100644 --- a/src/main/java/net/snackbag/mcvera/mixin/MouseMixin.java +++ b/src/main/java/net/snackbag/mcvera/mixin/MouseMixin.java @@ -2,7 +2,14 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.Mouse; +import net.snackbag.mcvera.MCVeraData; import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VCursorShape; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.VEvents; +import net.snackbag.vera.flag.VAppFlag; +import net.snackbag.vera.util.DragHandler; +import net.snackbag.vera.widget.VWidget; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -23,12 +30,19 @@ public abstract class MouseMixin { double scaleFactor = client.getWindow().getScaleFactor(); - int scaledX = (int) (fx / scaleFactor); - int scaledY = (int) (fy / scaleFactor); + int mouseX = (int) (fx / scaleFactor); + int mouseY = (int) (fy / scaleFactor); - Vera.forHoveredWidget(scaledX, scaledY, (widget) -> { - widget.fireEvent("mouse-move", scaledX, scaledY); + VeraApp top = MCVeraData.getTopHierarchy(); + Vera.forAllVisibleApps(app -> { + if (app.hasFlag(VAppFlag.HIERARCHIC) && app != top) return; + + VWidget widget = app.getTopWidgetAt(mouseX, mouseY); + if (widget != null) widget.events.fire(VEvents.Widget.MOUSE_MOVE, mouseX, mouseY); + else if (app.getCursorShape() != VCursorShape.DEFAULT) app.setCursorShape(VCursorShape.DEFAULT); }); + + DragHandler.move(); } @Inject(method = "onFilesDropped", at = @At("HEAD")) diff --git a/src/main/java/net/snackbag/mcvera/mixin/ParentElementMixin.java b/src/main/java/net/snackbag/mcvera/mixin/ParentElementMixin.java index b47fbf98..75bf473a 100644 --- a/src/main/java/net/snackbag/mcvera/mixin/ParentElementMixin.java +++ b/src/main/java/net/snackbag/mcvera/mixin/ParentElementMixin.java @@ -1,41 +1,121 @@ package net.snackbag.mcvera.mixin; import net.minecraft.client.gui.ParentElement; +import net.snackbag.mcvera.MCVeraData; import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VMouseButton; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.VEvents; +import net.snackbag.vera.flag.VAppFlag; +import net.snackbag.vera.util.DragHandler; +import net.snackbag.vera.widget.VWidget; +import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import java.util.ArrayList; +import java.util.List; + @Mixin(ParentElement.class) public interface ParentElementMixin { @Inject(method = "mouseClicked", at = @At("HEAD")) - private void mcvera$handleMouseClick(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { - Vera.forHoveredWidget((int) mouseX, (int) mouseY, (widget) -> { - switch (button) { - case 0: widget.fireEvent("left-click"); break; - case 1: widget.fireEvent("right-click"); break; - case 2: widget.fireEvent("middle-click"); break; - default: throw new IllegalStateException("Invalid button type: " + button); + private void mcvera$handleMouseClick(double mouseXRaw, double mouseYRaw, int button, CallbackInfoReturnable cir) { + int mouseX = (int) mouseXRaw; + int mouseY = (int) mouseYRaw; + boolean justChanged = false; + + VMouseButton btn = VMouseButton.fromInt(button); + + List hierarchicApps = new ArrayList<>(MCVeraData.getAppsWithFlag(VAppFlag.HIERARCHIC)); + for (VeraApp app : hierarchicApps) { + if (app.isPointOverThis(mouseX, mouseY)) { + if (MCVeraData.isTopHierarchy(app)) break; + + app.moveToHierarchyTop(); + justChanged = true; + break; } - }, (app) -> app.setFocusedWidget(null)); + } + + boolean finalJustChanged = justChanged; // weird java shit + MCVeraData.asTopHierarchy(app -> { + if (!app.isPointOverThis(mouseX, mouseY)) return; + if (finalJustChanged) return; + + handleClickEvents(app.getTopWidgetAt(mouseX, mouseY), btn); + }); + + Vera.forAllVisibleApps(app -> { + if (app.hasFlag(VAppFlag.HIERARCHIC)) return; + + VWidget hoveredWidget = app.getTopWidgetAt(mouseX, mouseY); + if (hoveredWidget != null) handleClickEvents(hoveredWidget, btn); + else app.setFocusedWidget(null); + }); + } + + @Unique + private void handleClickEvents(@Nullable VWidget widget, VMouseButton button) { + if (widget == null) return; + + switch (button) { + case LEFT -> widget.events.fire(VEvents.Widget.LEFT_CLICK); + case RIGHT -> widget.events.fire(VEvents.Widget.RIGHT_CLICK); + case MIDDLE -> widget.events.fire(VEvents.Widget.MIDDLE_CLICK); + } + + DragHandler.down(button, widget); } @Inject(method = "mouseReleased", at = @At("HEAD")) - private void mcvera$handleMouseRelease(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { - Vera.forHoveredWidget((int) mouseX, (int) mouseY, (widget) -> { - switch (button) { - case 0: widget.fireEvent("left-click-release"); break; - case 1: widget.fireEvent("right-click-release"); break; - case 2: widget.fireEvent("middle-click-release"); break; - } + private void mcvera$handleMouseRelease(double mouseXRaw, double mouseYRaw, int button, CallbackInfoReturnable cir) { + int mouseX = (int) mouseXRaw; + int mouseY = (int) mouseYRaw; + + VMouseButton btn = VMouseButton.fromInt(button); + + MCVeraData.asTopHierarchy(app -> handleReleaseEvents(app.getTopWidgetAt(mouseX, mouseY), btn)); + Vera.forAllVisibleApps(app -> { + if (app.hasFlag(VAppFlag.HIERARCHIC)) return; + if (!app.isPointOverThis(mouseX, mouseY)) return; + + handleReleaseEvents(app.getTopWidgetAt(mouseX, mouseY), btn); }); + + DragHandler.release(btn); + } + + @Unique + private void handleReleaseEvents(@Nullable VWidget widget, VMouseButton button) { + if (widget == null) return; + + switch (button) { + case LEFT -> widget.events.fire(VEvents.Widget.LEFT_CLICK_RELEASE); + case RIGHT -> widget.events.fire(VEvents.Widget.RIGHT_CLICK_RELEASE); + case MIDDLE -> widget.events.fire(VEvents.Widget.MIDDLE_CLICK_RELEASE); + } } @Inject(method = "mouseScrolled", at = @At("HEAD")) - private void mcvera$handleMouseScroll(double mouseX, double mouseY, double amount, CallbackInfoReturnable cir) { - Vera.forHoveredWidget((int) mouseX, (int) mouseY, (widget) -> { - widget.fireEvent("mouse-scroll", (int) mouseX, (int) mouseY, amount); + private void mcvera$handleMouseScroll(double mouseXRaw, double mouseYRaw, double amount, CallbackInfoReturnable cir) { + int mouseX = (int) mouseXRaw; + int mouseY = (int) mouseYRaw; + + MCVeraData.asTopHierarchy(app -> handleScrollEvents(app.getTopWidgetAt(mouseX, mouseY), mouseX, mouseY, amount)); + Vera.forAllVisibleApps(app -> { + if (app.hasFlag(VAppFlag.HIERARCHIC)) return; + if (!app.isPointOverThis(mouseX, mouseY)) return; + + handleScrollEvents(app.getTopWidgetAt(mouseX, mouseY), mouseX, mouseY, amount); }); } + + @Unique + private void handleScrollEvents(@Nullable VWidget widget, int x, int y, double amount) { + if (widget == null) return; + widget.events.fire(VEvents.Widget.SCROLL, x, y, amount); + } } diff --git a/src/main/java/net/snackbag/mcvera/mixin/VeraAppMixin.java b/src/main/java/net/snackbag/mcvera/mixin/VeraAppMixin.java deleted file mode 100644 index c0202c6a..00000000 --- a/src/main/java/net/snackbag/mcvera/mixin/VeraAppMixin.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.snackbag.mcvera.mixin; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; -import net.snackbag.mcvera.MCVeraData; -import net.snackbag.mcvera.impl.MCVeraProvider; -import net.snackbag.vera.core.VeraApp; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(VeraApp.class) -public abstract class VeraAppMixin { - @Inject(at = @At("HEAD"), method = "handleShortcut", remap = false) - private void mcvera$handleShortcut(String combination, CallbackInfo ci) { - VeraApp instance = (VeraApp) (Object) this; - if (!MCVeraData.debugApps.contains(instance)) return; - - MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.of("§e§l[DEBUG]§f: (shortcut) " + combination)); - } -} diff --git a/src/main/java/net/snackbag/mcvera/test/HierarchyTest.java b/src/main/java/net/snackbag/mcvera/test/HierarchyTest.java new file mode 100644 index 00000000..1eeefc53 --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/test/HierarchyTest.java @@ -0,0 +1,63 @@ +package net.snackbag.mcvera.test; + +import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VFont; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.VShortcut; +import net.snackbag.vera.flag.VAppFlag; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.widget.VLabel; +import net.snackbag.vera.widget.VRect; + +public class HierarchyTest { + public static HierarchyTest INSTANCE = new HierarchyTest(); + + public void start() { + Application first = new Application("first"); + Application second = new Application("second"); + Application third = new Application("third"); + + first.show(); + first.move(10); + + second.show(); + second.move(120, 10); + + third.show(); + third.move(230, 10); + + Vera.scheduleToNextFrame(first::moveToHierarchyTop); + } + + public static class Application extends VeraApp { + private final String name; + + public Application(String name) { + this.name = name; + } + + @Override + public void init() { + new VShortcut(this, "escape", this::hide); + + setBackgroundColor(VColor.MC_DARK_GRAY); + setFlag(VAppFlag.HIERARCHIC, true); + + styleSheet.setKey(VLabel.class, "font", VFont.create().withColor(VColor.white()).withSize(12)); + styleSheet.setKey(VLabel.class, "font", VFont.create().withColor(VColor.MC_AQUA).withSize(12), VStyleState.HOVERED); + styleSheet.setKey(VLabel.class, "transition", 250); + + VRect mover = new VRect(VColor.black(), 0, 0, 100, 8, this).alsoAdd(); + mover.onMouseDragLeft((ctx) -> move(getX() + ctx.moveX(), getY() + ctx.moveY())); + + VLabel label = new VLabel(name, this).alsoAdd(); + label.move(1); + } + + @Override + public void update() { + setSize(100, 200); + } + } +} diff --git a/src/main/java/net/snackbag/mcvera/test/LayoutCenteringTestApplication.java b/src/main/java/net/snackbag/mcvera/test/LayoutCenteringTestApplication.java new file mode 100644 index 00000000..de6c7aa8 --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/test/LayoutCenteringTestApplication.java @@ -0,0 +1,15 @@ +package net.snackbag.mcvera.test; + +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.VShortcut; + +public class LayoutCenteringTestApplication extends VeraApp { + public static LayoutCenteringTestApplication INSTANCE = new LayoutCenteringTestApplication(); + + @Override + public void init() { + new VShortcut(this, "escape", this::hide); + + + } +} diff --git a/src/main/java/net/snackbag/mcvera/test/LayoutTestApplication.java b/src/main/java/net/snackbag/mcvera/test/LayoutTestApplication.java new file mode 100644 index 00000000..f978d4b8 --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/test/LayoutTestApplication.java @@ -0,0 +1,29 @@ +package net.snackbag.mcvera.test; + +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.VShortcut; +import net.snackbag.vera.layout.VHLayout; +import net.snackbag.vera.layout.VLayout; +import net.snackbag.vera.layout.VVLayout; +import net.snackbag.vera.widget.VLabel; + +public class LayoutTestApplication extends VeraApp { + public static LayoutTestApplication INSTANCE = new LayoutTestApplication(); + + @Override + public void init() { + new VShortcut(this, "escape", this::hide); + + VLayout layout = new VVLayout(this, 0, 0); + new VLabel("I'm a test", this).alsoAddTo(layout); + new VLabel("I'm another test", this).alsoAddTo(layout); + + VLayout secondLayout = new VHLayout(layout); + new VLabel("1", this).alsoAddTo(secondLayout); + new VLabel("2", this).alsoAddTo(secondLayout); + + VLayout thirdLayout = new VVLayout(secondLayout); + new VLabel("oh?", this).alsoAddTo(thirdLayout); + new VLabel("oh!!!!", this).alsoAddTo(thirdLayout); + } +} diff --git a/src/main/java/net/snackbag/mcvera/test/QuadTestApplication.java b/src/main/java/net/snackbag/mcvera/test/QuadTestApplication.java new file mode 100644 index 00000000..216fbf2f --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/test/QuadTestApplication.java @@ -0,0 +1,36 @@ +package net.snackbag.mcvera.test; + +import net.minecraft.util.Identifier; +import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.VShortcut; + +public class QuadTestApplication extends VeraApp { + public static QuadTestApplication INSTANCE = new QuadTestApplication(); + + @Override + public void init() { + new VShortcut(this, "escape", this::hide); + } + + @Override + public void render() { + Vera.renderer.renderColQuad( + 0, 0, + 0, 100, + 100, 100, + 100, 0, + VColor.MC_RED + ); + + Vera.renderer.renderTexQuad( + true, + new Identifier("mcvera", "icon.png"), + 100, 0, + 120, 100, + 220, 100, + 200, 0 + ); + } +} diff --git a/src/main/java/net/snackbag/mcvera/test/StyleTestApplication.java b/src/main/java/net/snackbag/mcvera/test/StyleTestApplication.java new file mode 100644 index 00000000..777e3bf6 --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/test/StyleTestApplication.java @@ -0,0 +1,68 @@ +package net.snackbag.mcvera.test; + +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VCursorShape; +import net.snackbag.vera.core.VFont; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.VShortcut; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.style.animation.VAnimation; +import net.snackbag.vera.style.animation.VLoopMode; +import net.snackbag.vera.widget.VLabel; +import net.snackbag.vera.widget.VRect; + +public class StyleTestApplication extends VeraApp { + public static StyleTestApplication INSTANCE = new StyleTestApplication(); + + private final VAnimation longTestAnimation = new VAnimation.Builder("long_test") + .loopMode(VLoopMode.FORWARD_REPEAT) + .unwindTime(1000) + + .keyframe(1000, 2000, frame -> frame.style("background", VColor.MC_RED)) + .keyframe(1000, 5000, frame -> frame.style("background", VColor.MC_GOLD)) + .keyframe(1000, 1000, frame -> frame.style("background", VColor.MC_WHITE)) + .build(); + + @Override + public void init() { + new VShortcut(this, "escape", this::hide); + + mergeStyleSheet(createStyleSheet()); + + // Animations + VRect testRect = new VRect(VColor.black(), this).alsoAdd(); + testRect.move(10); + + testRect.onLeftClick(() -> testRect.animations.startOrRewind(longTestAnimation)); + testRect.onRightClick(() -> testRect.animations.unwind(longTestAnimation)); + + testRect.setStyle("transition", 100); + testRect.setStyle("background", VStyleState.HOVERED, VColor.white()); + testRect.setStyle("background", VStyleState.CLICKED, VColor.MC_RED); + + // Moving & classes + VLabel testLabel = new VLabel("hello there", 40, 10, this) + .alsoAddClass("label") + .alsoAdd(); + testLabel.setStyle("scale", VStyleState.DEFAULT, 1.0f); + testLabel.setStyle("scale", VStyleState.HOVERED, 1.2f); + testLabel.setStyle("scale", VStyleState.CLICKED, 2.0f); + testLabel.setStyle("transition", 100); + + testLabel.onMouseDragLeft((ctx) -> testLabel.move( + testLabel.getX() + ctx.moveX(), + testLabel.getY() + ctx.moveY() + )); + } + + public VStyleSheet createStyleSheet() { + VStyleSheet sheet = new VStyleSheet(); + + sheet.setKey("label", "font", VFont.create().withColor(VColor.MC_GOLD.sub(80))); + sheet.setKey("label", "font", VFont.create().withColor(VColor.MC_GOLD), VStyleState.HOVERED); + sheet.setKey("label", "cursor", VCursorShape.POINTING_HAND); + + return sheet; + } +} diff --git a/src/main/java/net/snackbag/mcvera/test/TestApplication.java b/src/main/java/net/snackbag/mcvera/test/TestApplication.java index 33e0458b..50049fef 100644 --- a/src/main/java/net/snackbag/mcvera/test/TestApplication.java +++ b/src/main/java/net/snackbag/mcvera/test/TestApplication.java @@ -1,24 +1,27 @@ package net.snackbag.mcvera.test; -import net.minecraft.client.MinecraftClient; import net.minecraft.util.Identifier; import net.snackbag.vera.Vera; -import net.snackbag.vera.core.VAlignmentFlag; +import net.snackbag.vera.core.VFont; +import net.snackbag.vera.core.VImage; +import net.snackbag.vera.flag.VHAlignmentFlag; import net.snackbag.vera.core.VColor; import net.snackbag.vera.core.VCursorShape; import net.snackbag.vera.core.VeraApp; import net.snackbag.vera.event.VShortcut; +import net.snackbag.vera.flag.VAppFlag; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.style.animation.VAnimation; import net.snackbag.vera.widget.*; -import org.lwjgl.PointerBuffer; -import org.lwjgl.system.MemoryStack; -import org.lwjgl.util.tinyfd.TinyFileDialogs; -import java.awt.*; import java.nio.file.Path; -import java.util.Arrays; public class TestApplication extends VeraApp { - public static final TestApplication INSTANCE = new TestApplication(); + public static TestApplication INSTANCE = new TestApplication(); + + private final VAnimation rotationAnimation = new VAnimation.Builder("rotation") + .keyframe(1000, 0, frame -> frame.style("rotation", 360f)) + .build(); public TestApplication() { super(); @@ -26,7 +29,7 @@ public TestApplication() { @Override public void init() { - VShortcut exit = new VShortcut(this, "escape", () -> { + new VShortcut(this, "escape", () -> { if (hasFocusedWidget()) { setFocusedWidget(null); return; @@ -35,37 +38,32 @@ public void init() { this.hide(); }); - VShortcut changeMouseRequired = new VShortcut(this, "leftalt+m", () -> { - setMouseRequired(!isMouseRequired()); - }); - - addShortcut(exit); - addShortcut(changeMouseRequired); + new VShortcut(this, "leftalt+m", () -> toggleFlag(VAppFlag.REQUIRES_MOUSE)); VLineInput input = new VLineInput(this).alsoAdd(); - input.setMaxChars(15); + input.setMaxChars(30); input.setPlaceholderText("Enter text..."); input.onAddCharLimited(System.out::println); input.onMouseMove((x, y) -> System.out.println("x=" + x + ", y=" + y)); input.move(50); - input.setBackgroundColor(VColor.white()); + input.setStyle("background", VColor.white()); setFocusedWidget(input); VLabel label = new VLabel("Hello world!", this).alsoAdd(); - label.onMouseDragLeft((oldX, oldY, newX, newY) -> setCursorShape(VCursorShape.VERTICAL_RESIZE)); + label.onMouseDragLeft((ctx) -> setCursorShape(VCursorShape.VERTICAL_RESIZE)); label.onLeftClickRelease(() -> setCursorShape(VCursorShape.DEFAULT)); - label.onMouseDragMiddle((oldX, oldY, newX, newY) -> setCursorShape(VCursorShape.ALL_RESIZE)); + label.onMouseDragMiddle((ctx) -> setCursorShape(VCursorShape.ALL_RESIZE)); label.onMiddleClickRelease(() -> setCursorShape(VCursorShape.DEFAULT)); - label.onMouseDragRight((oldX, oldY, newX, newY) -> setCursorShape(VCursorShape.HORIZONTAL_RESIZE)); + label.onMouseDragRight((ctx) -> setCursorShape(VCursorShape.HORIZONTAL_RESIZE)); label.onRightClickRelease(() -> setCursorShape(VCursorShape.DEFAULT)); label.onFilesDropped(System.out::println); - label.setPadding(5); + label.setStyle("padding", 5); label.move(10); - label.setBackgroundColor(VColor.black()); - label.setFont(label.getFont().withColor(VColor.white())); + label.setStyle("background", VColor.black()); + label.modifyFont().color(VColor.white()); label.adjustSize(); label.onHover(() -> { label.setText("Hovered"); @@ -75,30 +73,28 @@ public void init() { label.setText("Not hovered"); }); - VLabel centerLabel = new VLabel("CENTER", this).alsoAdd(); - centerLabel.setAlignment(VAlignmentFlag.CENTER); - centerLabel.setBackgroundColor(VColor.black()); + VLabel centerLabel = new VLabel("CENTER", 220, 10, 100, 16, this).alsoAdd(); + centerLabel.setAlignment(VHAlignmentFlag.CENTER); + centerLabel.setStyle("background", VColor.black()); centerLabel.modifyFontColor().rgb(255, 255, 255); - centerLabel.move(220, 10); - centerLabel.setBorder(VColor.MC_BLUE, VColor.MC_GOLD, VColor.MC_RED, VColor.MC_GREEN); - centerLabel.setBorderSize(5, 10, 8, 16); - centerLabel.setHoverCursor(VCursorShape.ALL_RESIZE); - - VLabel rightLabel = new VLabel("RIGHT", this).alsoAdd(); - rightLabel.setAlignment(VAlignmentFlag.RIGHT); - rightLabel.setBackgroundColor(VColor.black()); + centerLabel.setStyle("border-color", VColor.MC_BLUE, VColor.MC_GOLD, VColor.MC_RED, VColor.MC_GREEN); + centerLabel.setStyle("border-size", 5, 10, 8, 16); + centerLabel.setStyle("cursor", VCursorShape.ALL_RESIZE); + + VLabel rightLabel = new VLabel("RIGHT", 100, 10, 100, 16, this).alsoAdd(); + rightLabel.setAlignment(VHAlignmentFlag.RIGHT); + rightLabel.setStyle("background", VColor.black()); rightLabel.modifyFontColor().rgb(255, 255, 255); - rightLabel.move(100, 10); - rightLabel.setBorder(VColor.white()); - rightLabel.setBorderSize(1); + rightLabel.setStyle("border-color", VColor.white()); + rightLabel.setStyle("border-size", 1); rightLabel.onRightClick(() -> System.out.println(Vera.openFileSelector("test", Path.of("/Volumes/Media"), null))); - VImage image = new VImage( - Identifier.of(Identifier.DEFAULT_NAMESPACE, "textures/block/dirt.png"), - 32, 32, this).alsoAdd(); - image.move(0, 30); + VRect image = new VRect( + new VImage("minecraft:textures/block/dirt.png"), + 0, 30, 32, 32, this).alsoAdd(); image.onMiddleClick(this::hideCursor); image.onMiddleClickRelease(this::showCursor); + image.setStyle("background", VStyleState.HOVERED, "minecraft:textures/block/diamond_block.png"); VDropdown dropdown = new VDropdown(this).alsoAdd(); dropdown.addItem("coolio"); @@ -107,7 +103,7 @@ public void init() { dropdown.addItem("buger", () -> System.out.println("pressed")); dropdown.move(90); dropdown.setItemSpacing(16); - dropdown.modifyHoverFont().color(VColor.white()); + dropdown.itemHoverFont = VFont.create().withColor(VColor.white()); dropdown.setItemHoverColor(VColor.black()); dropdown.onFocusStateChange(() -> System.out.println("focus state change: " + dropdown.isFocused())); @@ -115,7 +111,7 @@ public void init() { VCheckBox checkbox = new VCheckBox(this).alsoAdd(); checkbox.move(20, 140); - checkbox.setHoverOverlayColor(VColor.white().withOpacity(0.4f)); + checkbox.setStyle("overlay", VStyleState.HOVERED, VColor.white().withOpacity(0.4f)); checkbox.onCheckStateChange((state) -> { if (!state) removeWidget(checkbox); @@ -128,10 +124,14 @@ public void init() { tabs.setActiveTab(0); VRect rotationRect = new VRect(VColor.black(), this).alsoAdd(); - rotationRect.rotate(45); + rotationRect.onLeftClick(() -> input.animate(rotationAnimation)); rotationRect.move(20, 200); } + private void toggleFlag(VAppFlag flag) { + setFlag(flag, !hasFlag(flag)); + } + @Override public void update() { super.update(); diff --git a/src/main/java/net/snackbag/mcvera/test/TestHandler.java b/src/main/java/net/snackbag/mcvera/test/TestHandler.java index 68a44829..f95f58b8 100644 --- a/src/main/java/net/snackbag/mcvera/test/TestHandler.java +++ b/src/main/java/net/snackbag/mcvera/test/TestHandler.java @@ -1,10 +1,8 @@ package net.snackbag.mcvera.test; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.util.InputUtil; -import org.lwjgl.glfw.GLFW; +import net.snackbag.mcvera.InternalCommands; public class TestHandler { public static boolean shouldTest() { @@ -14,10 +12,6 @@ public static boolean shouldTest() { public static void impl(boolean force) { if (!(force || shouldTest())) return; - ClientTickEvents.END_CLIENT_TICK.register((client) -> { - if (InputUtil.isKeyPressed(MinecraftClient.getInstance().getWindow().getHandle(), GLFW.GLFW_KEY_APOSTROPHE)) { - TestApplication.INSTANCE.setVisibility(true); - } - }); + ClientCommandRegistrationCallback.EVENT.register(InternalCommands::register); } } diff --git a/src/main/java/net/snackbag/vera/InternalVera.java b/src/main/java/net/snackbag/vera/InternalVera.java new file mode 100644 index 00000000..cee052e7 --- /dev/null +++ b/src/main/java/net/snackbag/vera/InternalVera.java @@ -0,0 +1,16 @@ +package net.snackbag.vera; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; + +@ApiStatus.Internal +public class InternalVera { + public static List getScheduledTasks() { + return Vera.nextFrameTasks; + } + + public static void clearScheduledTasks() { + Vera.nextFrameTasks.clear(); + } +} diff --git a/src/main/java/net/snackbag/vera/VElement.java b/src/main/java/net/snackbag/vera/VElement.java new file mode 100644 index 00000000..05e7e6a5 --- /dev/null +++ b/src/main/java/net/snackbag/vera/VElement.java @@ -0,0 +1,179 @@ +package net.snackbag.vera; + +import net.snackbag.vera.core.VAppAccess; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.EventHandler; +import net.snackbag.vera.event.VEvents; +import net.snackbag.vera.event.VWidgetMessageEvent; +import net.snackbag.vera.layout.VLayout; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public abstract class VElement { + protected int _x; + protected int _y; + protected int width; + protected int height; + + public boolean visible = true; + + public final EventHandler events; + public final VAppAccess appAccess; + private final List> visibilityConditions = new ArrayList<>(); + + protected @Nullable VLayout layout; + + public VElement(VAppAccess app, int x, int y, int width, int height) { + this.appAccess = app; + + this.events = new EventHandler(this); + this.events.preprocessor = this::handleBuiltinEvent; + this.events.postprocessor = this::afterBuiltinEvent; + + addVisibilityCondition(() -> visible); + + this._x = x; + this._y = y; + this.width = width; + this.height = height; + + onLayoutSwap(layout -> this.layout = layout); // event gets called. we use the event itself to change the layout + onLayoutRemove(() -> this.layout = null); + } + + public VeraApp getApp() { + return appAccess.get(); + } + + public void handleBuiltinEvent(String name, Object... args) {} + public void afterBuiltinEvent(String name, Object... args) {} + + // + // Visibility + // + + public boolean visibilityConditionsPassed() { + return visibilityConditions.parallelStream().allMatch(Supplier::get); + } + + public void addVisibilityCondition(Supplier condition) { + visibilityConditions.add(condition); + } + + public void hide() { + visible = false; + } + + public void show() { + visible = true; + } + + // + // Events + // + + public void onMessage(VWidgetMessageEvent executor) { + events.register(VEvents.Element.MESSAGE, args -> executor.run((VWidgetMessageEvent.Context) args[0])); + } + + public void sendMessage(VElement element, String type, @Nullable Object content) { + element.events.fire(VEvents.Element.MESSAGE, new VWidgetMessageEvent.Context(this, type, content)); + } + + public void onLayoutSwap(Consumer executor) { + events.register(VEvents.Element.LAYOUT_SWAP, args -> executor.accept((VLayout) args[0])); + } + + public void onLayoutRemove(Runnable executor) { + events.register(VEvents.Element.LAYOUT_REMOVE, args -> executor.run()); + } + + // + // Position & Size + // + + public int getX() { + return layout != null ? layout.posOf(this).x : _x; + } + + public int getY() { + return layout != null ? layout.posOf(this).y : _y; + } + + public final int getRawX() { + return _x; + } + + public final int getRawY() { + return _y; + } + + @Deprecated(forRemoval = true) + public int getEffectiveX() { + return getX(); + } + + @Deprecated(forRemoval = true) + public int getEffectiveY() { + return getY(); + } + + public int getRelativeMouseX() { + return Vera.getMouseX() - getX() - getApp().getX(); + } + + public int getRelativeMouseY() { + return Vera.getMouseY() - getY() - getApp().getY(); + } + + public void move(int both) { + move(both, both); + } + + public void move(int x, int y) { + this._x = x; + this._y = y; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getEffectiveWidth() { + return getWidth(); + } + + public int getEffectiveHeight() { + return getHeight(); + } + + public void setWidth(int width) { + setSize(width, height); + } + + public void setHeight(int height) { + setSize(width, height); + } + + public void setSize(int both) { + setSize(both, both); + } + + public void setSize(int width, int height) { + this.width = width; + this.height = height; + } + + public T alsoAddTo(VLayout layout) { + layout.addElement(this); + return (T) this; + } +} diff --git a/src/main/java/net/snackbag/vera/Vera.java b/src/main/java/net/snackbag/vera/Vera.java index 4b5bfbc3..eb3ce03e 100644 --- a/src/main/java/net/snackbag/vera/Vera.java +++ b/src/main/java/net/snackbag/vera/Vera.java @@ -3,47 +3,57 @@ import net.minecraft.client.MinecraftClient; import net.snackbag.mcvera.MCVeraData; import net.snackbag.mcvera.impl.MCVeraProvider; +import net.snackbag.mcvera.impl.MCVeraRegistrar; import net.snackbag.mcvera.impl.MCVeraRenderer; import net.snackbag.vera.core.VeraApp; -import net.snackbag.vera.widget.VWidget; +import net.snackbag.vera.flag.VAppFlag; +import net.snackbag.vera.flag.VAppPositioningFlag; import org.jetbrains.annotations.Nullable; import org.lwjgl.PointerBuffer; import org.lwjgl.system.MemoryStack; import org.lwjgl.util.tinyfd.TinyFileDialogs; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import java.util.function.Predicate; public class Vera { public static final MCVeraProvider provider = new MCVeraProvider(); public static final MCVeraRenderer renderer = new MCVeraRenderer(); + public static final MCVeraRegistrar registrar = new MCVeraRegistrar(); public static final String FONT_DEFAULT = provider.getDefaultFontName(); public static final String FONT_ARIAL = "minecraft:arial"; - public static void forHoveredWidget(int mouseX, int mouseY, Consumer> runnable, @Nullable Consumer ifEmpty) { - for (VeraApp app : MCVeraData.visibleApplications) { - List> hoveredWidgets = app.getHoveredWidgets(mouseX, mouseY); - if (hoveredWidgets.isEmpty()) { - if (ifEmpty != null) ifEmpty.accept(app); - return; - } + public static long renderCacheId = 0; + protected static final ArrayList nextFrameTasks = new ArrayList<>(); - for (VWidget widget : hoveredWidgets) { - if (widget.visibilityConditionsPassed()) runnable.accept(widget); - } + public static void forVisibleAndAllowedApps(Consumer handler) { + final List handledApps = new ArrayList<>(); + + VeraApp topHierarchy = MCVeraData.getTopHierarchy(); + if (topHierarchy != null) { + handledApps.add(topHierarchy); + handler.accept(topHierarchy); } - } - public static void forHoveredWidget(int mouseX, int mouseY, Consumer> runnable) { - forHoveredWidget(mouseX, mouseY, runnable, null); + for (VAppPositioningFlag flag : MCVeraData.visibleApplications.keySet()) { + for (VeraApp app : MCVeraData.visibleApplications.get(flag)) { + if (handledApps.contains(app) || app.hasFlag(VAppFlag.HIERARCHIC)) continue; + + handler.accept(app); + handledApps.add(app); + } + } } - public static void forHoveredWidgetIfEmpty(int mouseX, int mouseY, Consumer runnable) { - for (VeraApp app : MCVeraData.visibleApplications) { - List> hoveredWidgets = app.getHoveredWidgets(mouseX, mouseY); - if (hoveredWidgets.isEmpty()) runnable.accept(app); + public static void forAllVisibleApps(Consumer handler) { + for (VAppPositioningFlag flag : MCVeraData.visibleApplications.keySet()) { + for (VeraApp app : MCVeraData.visibleApplications.get(flag)) { + handler.accept(app); + } } } @@ -76,4 +86,22 @@ public static int getMouseY() { return path; } } + + @SafeVarargs + public static @Nullable T firstOf(Predicate evaluator, T... values) { + for (T v : values) { + if (v == null) continue; + if (evaluator.test(v)) return v; + } + + return null; + } + + /** + * Schedules a task to the next frame; run AFTER all Vera rendering + * @param runnable the task to execute + */ + public static void scheduleToNextFrame(Runnable runnable) { + nextFrameTasks.add(runnable); + } } diff --git a/src/main/java/net/snackbag/vera/core/VAlignmentFlag.java b/src/main/java/net/snackbag/vera/core/VAlignmentFlag.java deleted file mode 100644 index df5ca8b8..00000000 --- a/src/main/java/net/snackbag/vera/core/VAlignmentFlag.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.snackbag.vera.core; - -public enum VAlignmentFlag { - LEFT, - CENTER, - RIGHT -} diff --git a/src/main/java/net/snackbag/vera/core/VAppAccess.java b/src/main/java/net/snackbag/vera/core/VAppAccess.java new file mode 100644 index 00000000..34a3d43a --- /dev/null +++ b/src/main/java/net/snackbag/vera/core/VAppAccess.java @@ -0,0 +1,22 @@ +package net.snackbag.vera.core; + +import net.snackbag.vera.widget.VWidget; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public interface VAppAccess { + @NotNull + VeraApp get(); + + List> getWidgets(); + void removeWidget(VWidget widget); + void addWidget(VWidget widget); + + default List> getWidgetsReversed() { + List> widgets = getWidgets(); + Collections.reverse(widgets); + return widgets; + } +} diff --git a/src/main/java/net/snackbag/vera/core/VColor.java b/src/main/java/net/snackbag/vera/core/VColor.java index d3b2da74..55bfd4ca 100644 --- a/src/main/java/net/snackbag/vera/core/VColor.java +++ b/src/main/java/net/snackbag/vera/core/VColor.java @@ -1,8 +1,12 @@ package net.snackbag.vera.core; +import net.snackbag.vera.Vera; +import net.snackbag.vera.style.animation.easing.VEasing; +import org.jetbrains.annotations.ApiStatus; + import java.util.function.Consumer; -public class VColor { +public class VColor implements VFill { public static final VColor MC_BLACK = VColor.black(); public static final VColor MC_DARK_BLUE = VColor.of(0, 0, 170); public static final VColor MC_DARK_GREEN = VColor.of(0, 170, 0); @@ -69,43 +73,73 @@ public float opacity() { return opacity; } - public float oneRed() { + public float normRed() { return (float) red / 255; } - public float oneGreen() { + public float normGreen() { return (float) green / 255; } - public float oneBlue() { + public float normBlue() { return (float) blue / 255; } - public int opacityToAlpha() { + public int denormalizedOpacity() { return (int) (opacity * 255); } + /** + * Use {@link #isVisible()} instead. + * + * @return whether the color is transparent + */ + @Deprecated(since = "2.0") public boolean isTransparent() { return opacity == 0; } + @Deprecated(since = "2.0", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "2.1") public boolean sameColors(int red, int green, int blue) { - return this.red == red && this.green == green && this.blue == blue; + return hasSameColors(red, green, blue); } + @Deprecated(since = "2.0", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "2.1") public boolean sameColors(VColor color) { + return hasSameColors(color); + } + + public boolean hasSameColors(int red, int green, int blue) { + return this.red == red && this.green == green && this.blue == blue; + } + + public boolean hasSameColors(VColor color) { return sameColors(color.red, color.green, color.blue); } + @Deprecated(since = "2.0", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "2.1") public boolean same(int red, int green, int blue, float opacity) { - return sameColors(red, green, blue) && this.opacity == opacity; + return isSame(red, green, blue, opacity); } + @Deprecated(since = "2.0", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "2.1") public boolean same(VColor color) { + return isSame(color); + } + + public boolean isSame(int red, int green, int blue, float opacity) { + return hasSameColors(red, green, blue) && this.opacity == opacity; + } + + public boolean isSame(VColor color) { return same(color.red, color.green, color.blue, color.opacity); } - public int toInt() { + public int toIntArgb() { int alpha = (int) (opacity * 255); return (alpha << 24) | (red << 16) | (green << 8) | blue; } @@ -156,6 +190,30 @@ public VColor sub(int red, int green, int blue) { return new VColor(Math.max(this.red - red, 0), Math.max(this.green - green, 0), Math.max(this.blue - blue, 0)); } + @Override + public VFill ease(VEasing easing, VFill targetRaw, float delta) { + if (!(targetRaw instanceof VColor target)) { + throw new ClassCastException("Cannot ease two different types of VFill to color."); + } + + return new VColor( + easing.apply(red, target.red, delta), + easing.apply(green, target.green, delta), + easing.apply(blue, target.blue, delta), + easing.apply(opacity, target.opacity, delta) + ); + } + + @Override + public void renderQuad(VRenderContext ctx, int x, int y, int width, int height) { + Vera.renderer.drawRect(ctx, x, y, width, height, this); + } + + @Override + public boolean isVisible() { + return opacity != 0f; + } + public static VColor transparent() { return new VColor(0, 0, 0, 0); } @@ -189,6 +247,16 @@ public ColorModifier rgba(int r, int g, int b, float a) { return this; } + public ColorModifier all(int all) { + rgb(all, all, all); + return this; + } + + public ColorModifier rgb(VColor color) { + rgba(color.red, color.green, color.blue, color.opacity); + return this; + } + public ColorModifier red(int r) { color = color.withRed(r); colorUpdater.accept(color); diff --git a/src/main/java/net/snackbag/vera/core/VFill.java b/src/main/java/net/snackbag/vera/core/VFill.java new file mode 100644 index 00000000..6f893273 --- /dev/null +++ b/src/main/java/net/snackbag/vera/core/VFill.java @@ -0,0 +1,26 @@ +package net.snackbag.vera.core; + +import net.snackbag.vera.style.animation.easing.VEasing; + +public interface VFill { + static VFill empty() { + return new VFill() { + @Override + public void renderQuad(VRenderContext ctx, int x, int y, int width, int height) {} + + @Override + public VFill ease(VEasing easing, VFill target, float delta) { + return target; + } + + @Override + public boolean isVisible() { + return true; + } + }; + } + + void renderQuad(VRenderContext ctx, int x, int y, int width, int height); + VFill ease(VEasing easing, VFill target, float delta); + boolean isVisible(); +} diff --git a/src/main/java/net/snackbag/vera/core/VImage.java b/src/main/java/net/snackbag/vera/core/VImage.java new file mode 100644 index 00000000..344b08cd --- /dev/null +++ b/src/main/java/net/snackbag/vera/core/VImage.java @@ -0,0 +1,56 @@ +package net.snackbag.vera.core; + +import net.minecraft.util.Identifier; +import net.snackbag.vera.Vera; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.animation.easing.VEasing; + +public class VImage implements VFill { + public final Identifier src; + public final VColor tint; + + public VImage(String src, VColor tint) { + this(new Identifier(src), tint); + } + + public VImage(String src) { + this(new Identifier(src)); + } + + public VImage(Identifier src, VColor tint) { + this.src = src; + this.tint = tint; + } + + public VImage(Identifier src) { + this(src, VColor.white()); + } + + @Override + public void renderQuad(VRenderContext ctx, int x, int y, int width, int height) { + Vera.renderer.renderTexQuad( + ctx.hasTransparency, src, tint, + x, y, + x, y + height, + x + width, y + height, + x + width, y + ); + } + + @Override + public VFill ease(VEasing easing, VFill targetRaw, float delta) { + if (!(targetRaw instanceof VImage target)) { + throw new ClassCastException("Cannot ease two different types of VFill to image."); + } + + return new VImage( + (Identifier) StyleValueType.IDENTIFIER.animationTransition.apply(src, target.src, easing, delta), + (VColor) tint.ease(easing, target.tint, delta) + ); + } + + @Override + public boolean isVisible() { + return tint.isVisible(); + } +} diff --git a/src/main/java/net/snackbag/vera/core/VMouseButton.java b/src/main/java/net/snackbag/vera/core/VMouseButton.java new file mode 100644 index 00000000..c8d5e5d1 --- /dev/null +++ b/src/main/java/net/snackbag/vera/core/VMouseButton.java @@ -0,0 +1,16 @@ +package net.snackbag.vera.core; + +public enum VMouseButton { + LEFT, + MIDDLE, + RIGHT; + + public static VMouseButton fromInt(int button) { + return switch (button) { + case 0 -> LEFT; + case 1 -> RIGHT; + case 2 -> MIDDLE; + default -> throw new IllegalArgumentException("Invalid button type: %d".formatted(button)); + }; + } +} diff --git a/src/main/java/net/snackbag/vera/core/VRenderContext.java b/src/main/java/net/snackbag/vera/core/VRenderContext.java new file mode 100644 index 00000000..eac608b5 --- /dev/null +++ b/src/main/java/net/snackbag/vera/core/VRenderContext.java @@ -0,0 +1,88 @@ +package net.snackbag.vera.core; + +import net.snackbag.mcvera.impl.MCVeraRenderer; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Objects; + +public class VRenderContext { + public final int x; + public final int y; + public final int width; + public final int height; + public final float rotation; + public final float scale; + public final boolean hasTransparency; + + public VRenderContext( + int x, int y, + int width, int height, + float rotation, float scale, + boolean hasTransparency + ) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.rotation = rotation; + this.scale = scale; + this.hasTransparency = hasTransparency; + } + + private final ArrayList clipStack = new ArrayList<>(); + + public void withClip(int x, int y, int width, int height, Runnable exec) { + pushClip(x, y, width, height); + exec.run(); + popClip(); + } + + public void pushClip(int x, int y, int width, int height) { + clipStack.add(new ClipInstance(x, y, width, height)); + MCVeraRenderer.drawContext.enableScissor( + this.x + x, this.y + y, + this.x + x + width, this.y + y + height + ); + } + + public @Nullable ClipInstance peekClip() { + if (clipStack.isEmpty()) return null; + else return clipStack.get(clipStack.size() - 1); + } + + public void popClip() { + if (clipStack.isEmpty()) { + throw new IndexOutOfBoundsException("Cannot pop clip from VRenderContext, because there is nothing in the stack."); + } + + clipStack.remove(clipStack.size() - 1); + MCVeraRenderer.drawContext.disableScissor(); + } + + public void resetClips() { + for (ClipInstance ignored : clipStack) MCVeraRenderer.drawContext.disableScissor(); + clipStack.clear(); + } + + @Override + public int hashCode() { + return Objects.hash(x, y, width, height, rotation, scale, hasTransparency); + } + + @Override + public String toString() { + return "VRenderContext[" + + "x=" + x + ", " + + "y=" + y + ", " + + "width=" + width + ", " + + "height=" + height + ", " + + "rotation=" + rotation + ", " + + "scale=" + scale + ", " + + "hasTransparency=" + hasTransparency + ']'; + } + + + public record ClipInstance(int x, int y, int width, int height) { + } +} diff --git a/src/main/java/net/snackbag/vera/core/VeraApp.java b/src/main/java/net/snackbag/vera/core/VeraApp.java index 0e218746..b649e165 100644 --- a/src/main/java/net/snackbag/vera/core/VeraApp.java +++ b/src/main/java/net/snackbag/vera/core/VeraApp.java @@ -1,25 +1,30 @@ package net.snackbag.vera.core; import net.minecraft.client.MinecraftClient; +import net.snackbag.mcvera.MCVeraData; +import net.snackbag.mcvera.MinecraftVera; import net.snackbag.vera.Vera; +import net.snackbag.vera.event.VEvents; import net.snackbag.vera.event.VShortcut; +import net.snackbag.vera.flag.VAppFlag; +import net.snackbag.vera.flag.VAppPositioningFlag; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.util.VGeometry; import net.snackbag.vera.widget.VWidget; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.*; + +public abstract class VeraApp implements VAppAccess { + public final VStyleSheet styleSheet = new VStyleSheet(); -public abstract class VeraApp { private final List> widgets; private final HashMap shortcuts; private VColor backgroundColor; private VCursorShape cursorShape; private boolean cursorVisible; - private boolean mouseRequired; private int x; private int y; @@ -28,6 +33,7 @@ public abstract class VeraApp { private boolean visible; private @Nullable VWidget focusedWidget; + private VAppPositioningFlag positioning; public VeraApp() { this(true); @@ -39,7 +45,7 @@ public VeraApp(boolean mouseRequired) { this.backgroundColor = VColor.transparent(); this.cursorShape = VCursorShape.DEFAULT; this.cursorVisible = true; - this.mouseRequired = mouseRequired; + if (mouseRequired) setFlag(VAppFlag.REQUIRES_MOUSE, true); Vera.provider.handleAppInitialization(this); @@ -49,12 +55,18 @@ public VeraApp(boolean mouseRequired) { this.y = 0; this.visible = false; + setPositioning(VAppPositioningFlag.SCREEN); + } + + @Override + public @NotNull VeraApp get() { + return this; } public void setCursorVisible(boolean cursorVisible) { this.cursorVisible = cursorVisible; - if (!visible || !mouseRequired) return; + if (!visible || !hasFlag(VAppFlag.REQUIRES_MOUSE)) return; GLFW.glfwSetInputMode( MinecraftClient.getInstance().getWindow().getHandle(), GLFW.GLFW_CURSOR, @@ -73,23 +85,6 @@ public boolean isCursorVisible() { return cursorVisible; } - public boolean isMouseRequired() { - return mouseRequired; - } - - public void setMouseRequired(boolean mouseRequired) { - if (this.mouseRequired == mouseRequired) return; - - Vera.provider.handleAppSetMouseRequired(this, mouseRequired); - this.mouseRequired = mouseRequired; - - if (!visible || !mouseRequired) return; - GLFW.glfwSetInputMode( - MinecraftClient.getInstance().getWindow().getHandle(), - GLFW.GLFW_CURSOR, - cursorVisible ? GLFW.GLFW_CURSOR_NORMAL : GLFW.GLFW_CURSOR_HIDDEN); - } - public boolean isVisible() { return visible; } @@ -115,7 +110,7 @@ public void setVisibility(boolean visible) { this.visible = visible; if (visible) setCursorShape(cursorShape); - if (!visible || !mouseRequired) return; + if (!visible || !hasFlag(VAppFlag.REQUIRES_MOUSE)) return; GLFW.glfwSetInputMode( MinecraftClient.getInstance().getWindow().getHandle(), GLFW.GLFW_CURSOR, @@ -138,6 +133,15 @@ public void setWidth(int width) { this.width = width; } + public void setSize(int both) { + setSize(both, both); + } + + public void setSize(int width, int height) { + setWidth(width); + setHeight(height); + } + public void move(int x, int y) { this.x = x; this.y = y; @@ -155,24 +159,47 @@ public int getY() { return y; } + public void moveToHierarchyTop() { + if (!hasFlag(VAppFlag.HIERARCHIC)) { + MinecraftVera.LOGGER.warn("Failed to move app to top, because hierarchy isn't enabled"); + return; + } + + MCVeraData.appFlags.get(VAppFlag.HIERARCHIC).remove(this); + MCVeraData.appFlags.get(VAppFlag.HIERARCHIC).add(0, this); + } + public abstract void init(); + @Override public List> getWidgets() { return new ArrayList<>(widgets); } + @Override public void addWidget(VWidget widget) { + if (widgets.contains(widget)) { + MinecraftVera.LOGGER.error("Can't add widget %s to app %s, because it is already added" + .formatted(widget.toString(), getClass().getSimpleName())); + return; + } + this.widgets.add(widget); } + @Override public void removeWidget(VWidget widget) { - if (!widgets.contains(widget)) return; + if (!widgets.contains(widget)) { + MinecraftVera.LOGGER.error("Can't remove widget %s from app %s, because it wasn't added" + .formatted(widget.toString(), getClass().getSimpleName())); + return; + } if (isFocusedWidget(widget)) setFocusedWidget(null); - if (widget.isLeftClickDown()) widget.fireEvent("left-click-release"); - if (widget.isMiddleClickDown()) widget.fireEvent("middle-click-release"); - if (widget.isRightClickDown()) widget.fireEvent("right-click-release"); - if (widget.isHovered()) widget.fireEvent("hover-leave"); + if (widget.isLeftClickDown()) widget.events.fire(VEvents.Widget.LEFT_CLICK_RELEASE); + if (widget.isMiddleClickDown()) widget.events.fire(VEvents.Widget.MIDDLE_CLICK_RELEASE); + if (widget.isRightClickDown()) widget.events.fire(VEvents.Widget.RIGHT_CLICK_RELEASE); + if (widget.isHovered()) widget.events.fire(VEvents.Widget.HOVER_LEAVE); this.widgets.remove(widget); } @@ -186,11 +213,19 @@ public VColor getBackgroundColor() { } public void render() { - Vera.renderer.drawRect(this, 0, 0, width, height, 0, backgroundColor); + Vera.renderer.drawRect(x, y, width, height, backgroundColor); } public void renderAfterWidgets() {} + public void renderHierarchyOverlay() { + Vera.renderer.drawRect(x, y, width, height, + backgroundColor.isVisible() + ? VColor.black().withOpacity(0.2f) + : backgroundColor.sub(40).withOpacity(0.2f) + ); + } + public void update() {} public void addShortcut(VShortcut shortcut) { @@ -210,31 +245,30 @@ public List getShortcuts() { return List.copyOf(shortcuts.values()); } - public List> getHoveredWidgets() { - return getHoveredWidgets(Vera.provider.getMouseX(), Vera.provider.getMouseY()); - } + public @Nullable VWidget getTopWidgetAt(int px, int py) { + int mx = px - x; + int my = py - y; - public List> getHoveredWidgets(int mouseX, int mouseY) { - return getWidgets().parallelStream() - .filter(widget -> isMouseOverWidget(widget, mouseX, mouseY)) + return getWidgetsReversed().stream() + .filter(widget -> isPointOverWidget(widget, mx, my)) .filter(VWidget::visibilityConditionsPassed) - .collect(Collectors.toList()); + .findFirst().orElse(null); } - private boolean isMouseOverWidget(VWidget widget, int mouseX, int mouseY) { + private boolean isPointOverWidget(VWidget widget, int px, int py) { if (!widget.visibilityConditionsPassed()) return false; - int widgetX = widget.getHitboxX() + x; - int widgetY = widget.getHitboxY() + y; + int widgetX = widget.getHitboxX(); + int widgetY = widget.getHitboxY(); int widgetWidth = widget.getHitboxWidth(); int widgetHeight = widget.getHitboxHeight(); - return mouseX >= widgetX && mouseX <= widgetX + widgetWidth && - mouseY >= widgetY && mouseY <= widgetY + widgetHeight; + return VGeometry.isInBox(px, py, widgetX, widgetY, widgetWidth, widgetHeight); } - public boolean isMouseOverApp(int mouseX, int mouseY) { + public boolean isPointOverThis(int px, int py) { if (!isVisible()) return false; - return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; + + return VGeometry.isInBox(px, py, x, y, width, height); } public void setFocusedWidget(@Nullable VWidget widget) { @@ -242,8 +276,8 @@ public void setFocusedWidget(@Nullable VWidget widget) { VWidget oldWidget = this.focusedWidget; this.focusedWidget = widget; - if (oldWidget != null) oldWidget.fireEvent("focus-state-change"); - if (widget != null) widget.fireEvent("focus-state-change"); + if (oldWidget != null) oldWidget.events.fire(VEvents.Widget.FOCUS_STATE_CHANGE); + if (widget != null) widget.events.fire(VEvents.Widget.FOCUS_STATE_CHANGE); } } @@ -273,6 +307,27 @@ public void setCursorShape(VCursorShape cursorShape) { ); } + public VAppPositioningFlag getPositioning() { + return positioning; + } + + public void setPositioning(VAppPositioningFlag positioning) { + // make sure hashmaps exist + if (!MCVeraData.visibleApplications.containsKey(this.positioning)) + MCVeraData.visibleApplications.put(this.positioning, new LinkedHashSet<>()); + if (!MCVeraData.visibleApplications.containsKey(positioning)) + MCVeraData.visibleApplications.put(positioning, new LinkedHashSet<>()); + + // if visible, then we can also add the app itself + if (isVisible()) { + MCVeraData.visibleApplications.get(positioning).add(this); + } + + // doesn't matter if visible or not, we always remove it from its original + MCVeraData.visibleApplications.get(this.positioning).remove(this); + this.positioning = positioning; + } + public void keyPressed(int keyCode, int scanCode, int modifiers) { if (hasFocusedWidget()) getFocusedWidget().keyPressed(keyCode, scanCode, modifiers); } @@ -280,4 +335,34 @@ public void keyPressed(int keyCode, int scanCode, int modifiers) { public void charTyped(char chr, int modifiers) { if (hasFocusedWidget()) getFocusedWidget().charTyped(chr, modifiers); } + + public void mergeStyleSheet(VStyleSheet target) { + styleSheet.addSheet(target); + } + + public boolean hasFlag(VAppFlag flag) { + if (!MCVeraData.appFlags.containsKey(flag)) return false; + else return MCVeraData.appFlags.get(flag).contains(this); + } + + public void setFlag(VAppFlag flag, boolean enabled) { + if (enabled == hasFlag(flag)) return; // if nothing has to be changed, change nothing + + if (!enabled) MCVeraData.appFlags.get(flag).remove(this); + else { + if (!MCVeraData.appFlags.containsKey(flag)) MCVeraData.appFlags.put(flag, new ArrayList<>()); + MCVeraData.appFlags.get(flag).add(this); + } + + // handle mouse requirements + if (flag == VAppFlag.REQUIRES_MOUSE) { + Vera.provider.handleAppSetMouseRequired(this, enabled); + + if (!visible || !enabled) return; + GLFW.glfwSetInputMode( + MinecraftClient.getInstance().getWindow().getHandle(), + GLFW.GLFW_CURSOR, + cursorVisible ? GLFW.GLFW_CURSOR_NORMAL : GLFW.GLFW_CURSOR_HIDDEN); + } + } } diff --git a/src/main/java/net/snackbag/vera/core/V4Color.java b/src/main/java/net/snackbag/vera/core/v4/V4Color.java similarity index 93% rename from src/main/java/net/snackbag/vera/core/V4Color.java rename to src/main/java/net/snackbag/vera/core/v4/V4Color.java index ff12a1d9..e7eadca2 100644 --- a/src/main/java/net/snackbag/vera/core/V4Color.java +++ b/src/main/java/net/snackbag/vera/core/v4/V4Color.java @@ -1,4 +1,6 @@ -package net.snackbag.vera.core; +package net.snackbag.vera.core.v4; + +import net.snackbag.vera.core.VColor; public class V4Color { private final VColor v1; diff --git a/src/main/java/net/snackbag/vera/core/V4Int.java b/src/main/java/net/snackbag/vera/core/v4/V4Int.java similarity index 96% rename from src/main/java/net/snackbag/vera/core/V4Int.java rename to src/main/java/net/snackbag/vera/core/v4/V4Int.java index 44e64507..cfc42113 100644 --- a/src/main/java/net/snackbag/vera/core/V4Int.java +++ b/src/main/java/net/snackbag/vera/core/v4/V4Int.java @@ -1,4 +1,4 @@ -package net.snackbag.vera.core; +package net.snackbag.vera.core.v4; public class V4Int { private final int v1; diff --git a/src/main/java/net/snackbag/vera/event/EventHandler.java b/src/main/java/net/snackbag/vera/event/EventHandler.java new file mode 100644 index 00000000..0a01d5d5 --- /dev/null +++ b/src/main/java/net/snackbag/vera/event/EventHandler.java @@ -0,0 +1,58 @@ +package net.snackbag.vera.event; + +import net.snackbag.vera.VElement; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class EventHandler { + public final VElement element; + + public @Nullable EventHandler.Processor preprocessor; // called before event stack execution + public @Nullable EventHandler.Processor postprocessor; // called after event stack execution + + private final HashMap> executors = new HashMap<>(); + + public EventHandler(VElement element) { + this.element = element; + } + + public void fire(String name, Object... args) { + if (preprocessor != null) preprocessor.call(name, args); + + if (!executors.containsKey(name)) { + doPostProcessor(name, args); + return; + } + + executors.get(name).parallelStream().forEach(e -> e.run(args)); + doPostProcessor(name, args); + } + + private void doPostProcessor(String name, Object... args) { + if (postprocessor != null) postprocessor.call(name, args); + } + + public void register(String name, VEvent executor) { + executors.computeIfAbsent(name, k -> new ArrayList<>()).add(executor); + } + + public void register(String name, Runnable executor) { + register(name, e -> executor.run()); + } + + public void clear() { + executors.clear(); + } + + public void clear(String name) { + executors.remove(name); + } + + @FunctionalInterface + public interface Processor { + void call(String name, Object[] args); + } +} diff --git a/src/main/java/net/snackbag/vera/event/VAnimationBeginEvent.java b/src/main/java/net/snackbag/vera/event/VAnimationBeginEvent.java new file mode 100644 index 00000000..5743bba2 --- /dev/null +++ b/src/main/java/net/snackbag/vera/event/VAnimationBeginEvent.java @@ -0,0 +1,7 @@ +package net.snackbag.vera.event; + +import net.snackbag.vera.style.animation.VAnimation; + +public interface VAnimationBeginEvent { + void run(VAnimation animation); +} diff --git a/src/main/java/net/snackbag/vera/event/VAnimationFinishEvent.java b/src/main/java/net/snackbag/vera/event/VAnimationFinishEvent.java new file mode 100644 index 00000000..77d1886b --- /dev/null +++ b/src/main/java/net/snackbag/vera/event/VAnimationFinishEvent.java @@ -0,0 +1,7 @@ +package net.snackbag.vera.event; + +import net.snackbag.vera.style.animation.VAnimation; + +public interface VAnimationFinishEvent { + void run(VAnimation animation, long beginTime); +} diff --git a/src/main/java/net/snackbag/vera/event/VAnimationRewindEvent.java b/src/main/java/net/snackbag/vera/event/VAnimationRewindEvent.java new file mode 100644 index 00000000..ced849b2 --- /dev/null +++ b/src/main/java/net/snackbag/vera/event/VAnimationRewindEvent.java @@ -0,0 +1,7 @@ +package net.snackbag.vera.event; + +import net.snackbag.vera.style.animation.VAnimation; + +public interface VAnimationRewindEvent { + void run(VAnimation animation); +} diff --git a/src/main/java/net/snackbag/vera/event/VAnimationUnwindEvent.java b/src/main/java/net/snackbag/vera/event/VAnimationUnwindEvent.java new file mode 100644 index 00000000..d08f8b5f --- /dev/null +++ b/src/main/java/net/snackbag/vera/event/VAnimationUnwindEvent.java @@ -0,0 +1,7 @@ +package net.snackbag.vera.event; + +import net.snackbag.vera.style.animation.VAnimation; + +public interface VAnimationUnwindEvent { + void run(VAnimation animation); +} diff --git a/src/main/java/net/snackbag/vera/event/VEvents.java b/src/main/java/net/snackbag/vera/event/VEvents.java new file mode 100644 index 00000000..2883c5de --- /dev/null +++ b/src/main/java/net/snackbag/vera/event/VEvents.java @@ -0,0 +1,75 @@ +package net.snackbag.vera.event; + +// Sorted by :sparkle: the feeling that it looks nice :sparkle: +public class VEvents { + // Animation + public static class Animation { + public static final String BEGIN = "animation-begin"; + public static final String FINISH = "animation-finish"; + public static final String UNWIND_BEGIN = "animation-unwind-begin"; + public static final String REWIND_BEGIN = "animation-rewind-begin"; + } + + // Element + public static class Element { + public static final String MESSAGE = "elem-message"; + public static final String LAYOUT_SWAP = "elem-layout-swap"; + public static final String LAYOUT_REMOVE = "elem-layout-remove"; + } + + // Widget + public static class Widget { + public static final String HOVER = "hover"; + public static final String HOVER_LEAVE = "hover-leave"; + + public static final String LEFT_CLICK = "left-click"; + public static final String LEFT_CLICK_RELEASE = "left-click-release"; + public static final String MIDDLE_CLICK = "middle-click"; + public static final String MIDDLE_CLICK_RELEASE = "middle-click-release"; + public static final String RIGHT_CLICK = "right-click"; + public static final String RIGHT_CLICK_RELEASE = "right-click-release"; + + public static final String SCROLL = "mouse-scroll"; + public static final String MOUSE_MOVE = "mouse-move"; + public static final String DRAG_LEFT_CLICK = "mouse-drag-left"; + public static final String DRAG_RIGHT_CLICK = "mouse-drag-right"; + public static final String DRAG_MIDDLE_CLICK = "mouse-drag-middle"; + + public static final String TRANSPARENCY_STATE_CHANGE = "transparency-state-change"; + + public static final String FOCUS_STATE_CHANGE = "focus-state-change"; + public static final String FILES_DROPPED = "files-dropped"; + } + + // Checkbox + public static class CheckBox { + public static final String CHECK_STATE_CHANGED = "vcheckbox-check-state-changed"; + } + + // Dropdown + public static class Dropdown { + public static final String ITEM_SWITCH = "vdropdown-item-switch"; + public static final String SELECTOR_OPEN = "vdropdown-selector-open"; + public static final String SELECTOR_CLOSE = "vdropdown-selector-close"; + } + + // Line input + public static class LineInput { + public static final String CHANGE = "vline-change"; + public static final String CURSOR_MOVE = "vline-cursor-move"; + public static final String CURSOR_MOVE_LEFT = "vline-cursor-move-left"; + public static final String CURSOR_MOVE_RIGHT = "vline-cursor-move-right"; + public static final String ADD_CHAR_LIMITED = "vline-add-char-limited"; + } + + // Tabs + public static class TabWidget { + public static final String TAB_HOVER_CHANGE = "vtabwidget-tab-hover-change"; + public static final String TAB_LEFT_CLICK = "vtabwidget-tab-left-click"; + public static final String TAB_LEFT_CLICK_RELEASE = "vtabwidget-tab-left-click-release"; + public static final String TAB_MIDDLE_CLICK = "vtabwidget-tab-middle-click"; + public static final String TAB_MIDDLE_CLICK_RELEASE = "vtabwidget-tab-middle-click-release"; + public static final String TAB_RIGHT_CLICK = "vtabwidget-tab-right-click"; + public static final String TAB_RIGHT_CLICK_RELEASE = "vtabwidget-tab-right-click-release"; + } +} diff --git a/src/main/java/net/snackbag/vera/event/VMouseDragEvent.java b/src/main/java/net/snackbag/vera/event/VMouseDragEvent.java index d22d00fd..c2a32bd7 100644 --- a/src/main/java/net/snackbag/vera/event/VMouseDragEvent.java +++ b/src/main/java/net/snackbag/vera/event/VMouseDragEvent.java @@ -1,5 +1,15 @@ package net.snackbag.vera.event; public interface VMouseDragEvent { - void run(int startX, int startY, int currentX, int currentY); + void run(Context ctx); + + record Context(int startX, int startY, int currentX, int currentY, int moveX, int moveY, Direction direction) { + } + + enum Direction { + UP, + DOWN, + LEFT, + RIGHT + } } diff --git a/src/main/java/net/snackbag/vera/event/VShortcut.java b/src/main/java/net/snackbag/vera/event/VShortcut.java index cc7cc76d..d519f207 100644 --- a/src/main/java/net/snackbag/vera/event/VShortcut.java +++ b/src/main/java/net/snackbag/vera/event/VShortcut.java @@ -3,11 +3,12 @@ import net.snackbag.vera.Vera; import net.snackbag.vera.core.VeraApp; import org.apache.commons.lang3.SystemUtils; +import org.jetbrains.annotations.ApiStatus; public class VShortcut { - private final VeraApp app; + public final VeraApp app; private final String combination; - private final boolean transformOSX; + public final boolean transformOSX; private Runnable event; public VShortcut(VeraApp app, String combination, Runnable event) { @@ -19,10 +20,8 @@ public VShortcut(VeraApp app, String combination, Runnable event, boolean transf this.combination = combination.toLowerCase().replace(" ", ""); this.event = event; this.transformOSX = transformOSX; - } - public VeraApp getApp() { - return app; + this.app.addShortcut(this); } public String getCombination() { @@ -45,14 +44,16 @@ public void setEvent(Runnable event) { this.event = event; } - public boolean shouldTransformOSX() { - return transformOSX; - } - public void run() { Vera.provider.handleRunShortcut(this); } + /** + * Deprecated since version 2.0, will be removed in 2.1. No longer needed since the app now already receives the + * shortcut on shortcut initialization. + */ + @Deprecated(forRemoval = true, since = "2.0") + @ApiStatus.ScheduledForRemoval(inVersion = "2.1") public VShortcut alsoAdd() { app.addShortcut(this); return this; diff --git a/src/main/java/net/snackbag/vera/event/VTransparencyStateChangeEvent.java b/src/main/java/net/snackbag/vera/event/VTransparencyStateChangeEvent.java new file mode 100644 index 00000000..d5fd41bb --- /dev/null +++ b/src/main/java/net/snackbag/vera/event/VTransparencyStateChangeEvent.java @@ -0,0 +1,5 @@ +package net.snackbag.vera.event; + +public interface VTransparencyStateChangeEvent { + void run(boolean transparency); +} diff --git a/src/main/java/net/snackbag/vera/event/VWidgetMessageEvent.java b/src/main/java/net/snackbag/vera/event/VWidgetMessageEvent.java index 39050b1e..ec13ab00 100644 --- a/src/main/java/net/snackbag/vera/event/VWidgetMessageEvent.java +++ b/src/main/java/net/snackbag/vera/event/VWidgetMessageEvent.java @@ -1,6 +1,6 @@ package net.snackbag.vera.event; -import net.snackbag.vera.widget.VWidget; +import net.snackbag.vera.VElement; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -8,27 +8,17 @@ public interface VWidgetMessageEvent { void run(Context ctx); - class Context { - public final @NotNull VWidget sender; - public final @NotNull String type; - public final @Nullable Object content; - - public Context(@NotNull VWidget sender, @NotNull String type, @Nullable Object content) { - this.sender = sender; - this.type = type; - this.content = content; - } - + record Context(@NotNull VElement sender, @NotNull String type, @Nullable Object content) { public boolean isContentNull() { - return content == null; - } + return content == null; + } - public boolean isContentString() { - return content != null && content instanceof String; - } + public boolean isContentString() { + return content != null && content instanceof String; + } - public T getContentOrDefault(T default_) { - return content == null ? default_ : (T) content; + public T getContentOrDefault(T default_) { + return content == null ? default_ : (T) content; + } } - } } diff --git a/src/main/java/net/snackbag/vera/flag/VAppFlag.java b/src/main/java/net/snackbag/vera/flag/VAppFlag.java new file mode 100644 index 00000000..25629499 --- /dev/null +++ b/src/main/java/net/snackbag/vera/flag/VAppFlag.java @@ -0,0 +1,7 @@ +package net.snackbag.vera.flag; + +public enum VAppFlag { + DEBUG, + HIERARCHIC, + REQUIRES_MOUSE +} diff --git a/src/main/java/net/snackbag/vera/flag/VAppPositioningFlag.java b/src/main/java/net/snackbag/vera/flag/VAppPositioningFlag.java new file mode 100644 index 00000000..f7487d3d --- /dev/null +++ b/src/main/java/net/snackbag/vera/flag/VAppPositioningFlag.java @@ -0,0 +1,53 @@ +package net.snackbag.vera.flag; + +public enum VAppPositioningFlag { + /** + * Deepest render position, renders even under the vignette + */ + BELOW_VIGNETTE, + + /** + * Renders below everything, also under the spyglass + */ + BELOW_OVERLAYS, + + /** + * Renders under the HUD + */ + BELOW_HUD, + + /** + * Renders on the same level as the HUD + */ + HUD, + + /** + * Renders over the HUD and under normal GUI + */ + ABOVE_HUD, + + /** + * Renders on the same layer as the GUI + *

+ * Watch out: if the UI doesn't require a mouse to exist, it will not render if no other app that + * does require a mouse is present. In case you 100% need it, it is recommended to either use {@link #ABOVE_HUD} or + * {@link #ABOVE_GUI} + */ + GUI, + + /** + * Renders over the GUI but under any other render events + */ + ABOVE_GUI, + + /** + * (default)
+ * Renders over all other render events + */ + SCREEN, + + /** + * Should only be used when wanting to 100% override something + */ + TOP +} diff --git a/src/main/java/net/snackbag/vera/flag/VHAlignmentFlag.java b/src/main/java/net/snackbag/vera/flag/VHAlignmentFlag.java new file mode 100644 index 00000000..aaf09c5c --- /dev/null +++ b/src/main/java/net/snackbag/vera/flag/VHAlignmentFlag.java @@ -0,0 +1,7 @@ +package net.snackbag.vera.flag; + +public enum VHAlignmentFlag { + LEFT, + CENTER, + RIGHT +} diff --git a/src/main/java/net/snackbag/vera/flag/VLayoutAlignmentFlag.java b/src/main/java/net/snackbag/vera/flag/VLayoutAlignmentFlag.java new file mode 100644 index 00000000..9114ba3b --- /dev/null +++ b/src/main/java/net/snackbag/vera/flag/VLayoutAlignmentFlag.java @@ -0,0 +1,7 @@ +package net.snackbag.vera.flag; + +public enum VLayoutAlignmentFlag { + START, + CENTER, + END +} diff --git a/src/main/java/net/snackbag/vera/flag/VVAlignmentFlag.java b/src/main/java/net/snackbag/vera/flag/VVAlignmentFlag.java new file mode 100644 index 00000000..d98feff6 --- /dev/null +++ b/src/main/java/net/snackbag/vera/flag/VVAlignmentFlag.java @@ -0,0 +1,7 @@ +package net.snackbag.vera.flag; + +public enum VVAlignmentFlag { + TOP, + CENTER, + BOTTOM +} diff --git a/src/main/java/net/snackbag/vera/layout/VHLayout.java b/src/main/java/net/snackbag/vera/layout/VHLayout.java new file mode 100644 index 00000000..90032d61 --- /dev/null +++ b/src/main/java/net/snackbag/vera/layout/VHLayout.java @@ -0,0 +1,44 @@ +package net.snackbag.vera.layout; + +import net.snackbag.vera.VElement; +import net.snackbag.vera.core.VAppAccess; +import org.joml.Vector2i; + +public class VHLayout extends VLayout { + public VHLayout(VAppAccess app, int x, int y, int width, int height) { + super(app, x, y, width, height); + } + + public VHLayout(VAppAccess app, int x, int y) { + this(app, x, y, -1, -1); + } + + public VHLayout(VLayout parent, int width, int height) { + this(parent.appAccess, 0, 0, width, height); + this.alsoAddTo(parent); + } + + public VHLayout(VLayout parent) { + this(parent, -1, -1); + } + + @Override + public void rebuild() { + int x = getX(); + + for (VElement elem : elements) { + cache.put(elem, new Vector2i(x, getY())); + + x += elem.getEffectiveWidth(); + } + } + + @Override + protected Vector2i applyAlignment(Vector2i original) { + return switch (alignment) { + case START -> original; + case CENTER -> new Vector2i(getEffectiveWidth() / 2 - calculateElementsWidth() / 2 + original.x, original.y); + case END -> new Vector2i(getEffectiveWidth() - calculateElementsWidth() + original.x, original.y); + }; + } +} diff --git a/src/main/java/net/snackbag/vera/layout/VLayout.java b/src/main/java/net/snackbag/vera/layout/VLayout.java new file mode 100644 index 00000000..3686f6ec --- /dev/null +++ b/src/main/java/net/snackbag/vera/layout/VLayout.java @@ -0,0 +1,88 @@ +package net.snackbag.vera.layout; + +import net.snackbag.vera.VElement; +import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VAppAccess; +import net.snackbag.vera.event.VEvents; +import net.snackbag.vera.flag.VLayoutAlignmentFlag; +import org.joml.Vector2i; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public abstract class VLayout extends VElement { + protected final List elements = new ArrayList<>(); + public VLayoutAlignmentFlag alignment = VLayoutAlignmentFlag.START; + + /** + * ID for the last position cache call. To be compared with the current render cache ID + * + * @reason So we don't calculate the positions of widgets for each getX or getY call + */ + private long cacheId = 0; + protected final HashMap cache = new HashMap<>(); + + public VLayout(VAppAccess app, int x, int y, int width, int height) { + super(app, x, y, width, height); + } + + @Override + public int getWidth() { + return width < 0 ? calculateElementsWidth() : width; + } + + @Override + public int getHeight() { + return height < 0 ? calculateElementsHeight() : height; + } + + public Vector2i posOf(VElement elem) { + checkCache(); + + if (!cache.containsKey(elem)) throw new RuntimeException("Layout cache does not contain requested element '%s'".formatted(elem.toString())); + return applyAlignment(cache.get(elem)); + } + + private void checkCache() { + if (cacheId != Vera.renderCacheId) { + cache.clear(); + cacheId = Vera.renderCacheId; + + rebuild(); + } + } + + protected abstract Vector2i applyAlignment(Vector2i original); + public abstract void rebuild(); + + public int calculateElementsHeight() { + return elements.stream() + .mapToInt(VElement::getEffectiveHeight) + .sum(); + } + + public int calculateElementsWidth() { + return elements.stream() + .mapToInt(VElement::getEffectiveWidth) + .max().orElse(1); + } + + public void addElement(VElement elem) { + if (elements.contains(elem)) return; + elements.add(elem); + elem.events.fire(VEvents.Element.LAYOUT_SWAP, this); + } + + public boolean removeElement(VElement elem) { + if (!elements.contains(elem)) return false; + + elem.events.fire(VEvents.Element.LAYOUT_REMOVE); + return elements.remove(elem); + } + + public void clear() { + for (VElement elem : elements) elem.events.fire(VEvents.Element.LAYOUT_REMOVE); + elements.clear(); + } +} diff --git a/src/main/java/net/snackbag/vera/layout/VVLayout.java b/src/main/java/net/snackbag/vera/layout/VVLayout.java new file mode 100644 index 00000000..c8b7e29d --- /dev/null +++ b/src/main/java/net/snackbag/vera/layout/VVLayout.java @@ -0,0 +1,44 @@ +package net.snackbag.vera.layout; + +import net.snackbag.vera.VElement; +import net.snackbag.vera.core.VAppAccess; +import org.joml.Vector2i; + +public class VVLayout extends VLayout { + public VVLayout(VAppAccess app, int x, int y, int width, int height) { + super(app, x, y, width, height); + } + + public VVLayout(VAppAccess app, int x, int y) { + this(app, x, y, -1, -1); + } + + public VVLayout(VLayout parent, int width, int height) { + this(parent.appAccess, 0, 0, width, height); + this.alsoAddTo(parent); + } + + public VVLayout(VLayout parent) { + this(parent, -1, -1); + } + + @Override + public void rebuild() { + int y = getY(); + + for (VElement elem : elements) { + cache.put(elem, new Vector2i(getX(), y)); + + y += elem.getEffectiveHeight(); + } + } + + @Override + protected Vector2i applyAlignment(Vector2i original) { + return switch (alignment) { + case START -> original; + case CENTER -> new Vector2i(original.x, getEffectiveHeight() / 2 - calculateElementsHeight() / 2 + original.y); + case END -> new Vector2i(original.x, getEffectiveHeight() - calculateElementsHeight() + original.y); + }; + } +} diff --git a/src/main/java/net/snackbag/vera/layout/VXLayout.java b/src/main/java/net/snackbag/vera/layout/VXLayout.java new file mode 100644 index 00000000..82d52a37 --- /dev/null +++ b/src/main/java/net/snackbag/vera/layout/VXLayout.java @@ -0,0 +1,36 @@ +package net.snackbag.vera.layout; + +import net.snackbag.vera.VElement; +import net.snackbag.vera.core.VAppAccess; +import org.joml.Vector2i; + +public class VXLayout extends VLayout { + public VXLayout(VAppAccess app, int x, int y, int width, int height) { + super(app, x, y, width, height); + } + + public VXLayout(VAppAccess app, int x, int y) { + this(app, x, y, -1, -1); + } + + public VXLayout(VLayout parent, int width, int height) { + this(parent.appAccess, 0, 0, width, height); + this.alsoAddTo(parent); + } + + public VXLayout(VLayout parent) { + this(parent, -1, -1); + } + + @Override + protected Vector2i applyAlignment(Vector2i original) { + return original; + } + + @Override + public void rebuild() { + for (VElement elem : elements) { + cache.put(elem, new Vector2i(getX() + elem.getRawX(), getY() + elem.getRawY())); + } + } +} diff --git a/src/main/java/net/snackbag/vera/modifier/VHasFont.java b/src/main/java/net/snackbag/vera/modifier/VHasFont.java new file mode 100644 index 00000000..ce677b85 --- /dev/null +++ b/src/main/java/net/snackbag/vera/modifier/VHasFont.java @@ -0,0 +1,25 @@ +package net.snackbag.vera.modifier; + +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VFont; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.widget.VWidget; +import org.jetbrains.annotations.Nullable; + +public interface VHasFont extends VModifier { + default VFont.FontModifier modifyFont() { + return modifyFont(null); + } + + default VFont.FontModifier modifyFont(@Nullable VStyleState state) { + return getApp().styleSheet.modifyKeyAsFont((VWidget) this, "font", state); + } + + default VColor.ColorModifier modifyFontColor() { + return modifyFontColor(null); + } + + default VColor.ColorModifier modifyFontColor(@Nullable VStyleState state) { + return getApp().styleSheet.modifyKeyAsFontColor((VWidget) this, "font", state); + } +} diff --git a/src/main/java/net/snackbag/vera/modifier/VHasPlaceholderFont.java b/src/main/java/net/snackbag/vera/modifier/VHasPlaceholderFont.java new file mode 100644 index 00000000..bae2a5ef --- /dev/null +++ b/src/main/java/net/snackbag/vera/modifier/VHasPlaceholderFont.java @@ -0,0 +1,25 @@ +package net.snackbag.vera.modifier; + +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VFont; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.widget.VWidget; +import org.jetbrains.annotations.Nullable; + +public interface VHasPlaceholderFont extends VModifier { + default VFont.FontModifier modifyPlaceholderFont() { + return modifyPlaceholderFont(null); + } + + default VFont.FontModifier modifyPlaceholderFont(@Nullable VStyleState state) { + return getApp().styleSheet.modifyKeyAsFont((VWidget) this, "placeholder-font", state); + } + + default VColor.ColorModifier modifyPlaceholderFontColor() { + return modifyPlaceholderFontColor(null); + } + + default VColor.ColorModifier modifyPlaceholderFontColor(@Nullable VStyleState state) { + return getApp().styleSheet.modifyKeyAsFontColor((VWidget) this, "placeholder-font", state); + } +} diff --git a/src/main/java/net/snackbag/vera/modifier/VModifier.java b/src/main/java/net/snackbag/vera/modifier/VModifier.java new file mode 100644 index 00000000..4b7e714a --- /dev/null +++ b/src/main/java/net/snackbag/vera/modifier/VModifier.java @@ -0,0 +1,7 @@ +package net.snackbag.vera.modifier; + +import net.snackbag.vera.core.VeraApp; + +public interface VModifier { + VeraApp getApp(); +} diff --git a/src/main/java/net/snackbag/vera/modifier/VPaddingWidget.java b/src/main/java/net/snackbag/vera/modifier/VPaddingWidget.java index 5a71a1e7..346a2d53 100644 --- a/src/main/java/net/snackbag/vera/modifier/VPaddingWidget.java +++ b/src/main/java/net/snackbag/vera/modifier/VPaddingWidget.java @@ -1,7 +1,10 @@ package net.snackbag.vera.modifier; -import net.snackbag.vera.core.V4Int; +import net.snackbag.vera.core.v4.V4Int; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.ScheduledForRemoval(inVersion = "2.1") +@Deprecated(forRemoval = true, since = "2.0") public interface VPaddingWidget { V4Int getPadding(); diff --git a/src/main/java/net/snackbag/vera/style/StyleContainer.java b/src/main/java/net/snackbag/vera/style/StyleContainer.java new file mode 100644 index 00000000..98523b47 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/StyleContainer.java @@ -0,0 +1,169 @@ +package net.snackbag.vera.style; + +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +/** + * Holds types, keys and states. + *

+ * Consists of:
+ * | Part
+ * |--- Key
+ * |------ VStyleState:Object + */ +public class StyleContainer { + private final HashMap>> values = new HashMap<>(); + + public StyleContainer() {} + + public boolean hasPart(T part) { + return values.containsKey(part); + } + + public HashMap> getPart(T part) { + return values.getOrDefault(part, new HashMap<>()); + } + + public boolean hasKey(T part, String key) { + return getPart(part).containsKey(key); + } + + public HashMap getKey(T part, String key) { + return getPart(part).getOrDefault(key, new HashMap<>()); + } + + public boolean hasState(T part, String key, VStyleState state) { + return getKey(part, key).containsKey(state); + } + + public V getState(T part, String key, VStyleState state) { + return (V) getKey(part, key).get(state); + } + + + /** + * In this case, exact means that it does not resolve lower states and only + * gives the keys of exactly the given style state. Use {@link #getKeysStacked(Object, VStyleState)} + * for deeper state resolve. + *

+ * For example when requesting state HOVERED: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
KeyStateReturned
src + * DEFAULT + * No + *
overlay + * HOVERED + * Yes + *
fontCLICKEDNo
+ * + * @see #getKeysStacked(Object, VStyleState) + */ + public Set getKeysExact(T part, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + + Set buffer = new HashSet<>(); + + var resolvedPart = getPart(part); // i'm sorry for using var but holy fuck + for (String key : resolvedPart.keySet()) { + for (VStyleState keyState : resolvedPart.get(key).keySet()) { + if (keyState != state) continue; + buffer.add(key); + } + } + + return buffer; + } + + /** + * In this case, stacked means that also all keys from states below the + * given state are returned. Use {@link #getKeysExact(Object, VStyleState)} for + * only the exact keys of a style state. + *

+ * For example when requesting state HOVERED: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
KeyStateReturned
src + * DEFAULT + * Yes + *
overlay + * HOVERED + * Yes + *
fontCLICKEDNo
+ * + * @see #getKeysExact(Object, VStyleState) + */ + public Set getKeysStacked(T part, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + + Set buffer = new HashSet<>(); + + VStyleState next = state; + while (next != null) { + buffer.addAll(getKeysExact(part, next)); + next = next.fallback; + } + + return buffer; + } + + public void put(T part, String key, VStyleState state, Object value) { + if (!hasPart(part)) values.put(part, new HashMap<>()); + if (!hasKey(part, key)) values.get(part).put(key, new HashMap<>()); + if (!hasState(part, key, state)) values.get(part).get(key).put(state, new HashMap<>()); + + values.get(part).get(key).put(state, value); + } + + public void moldWith(StyleContainer target) { + for (T targetPart : target.values.keySet()) { + if (!hasPart(targetPart)) { + values.put(targetPart, target.getPart(targetPart)); + continue; + } + + for (String targetKey : target.getPart(targetPart).keySet()) { + if (!hasKey(targetPart, targetKey)) { + values.get(targetPart).put(targetKey, target.getKey(targetPart, targetKey)); + continue; + } + + for (VStyleState targetState : target.getKey(targetPart, targetKey).keySet()) { + if (!hasState(targetPart, targetKey, targetState)) { + values.get(targetPart).get(targetKey).put(targetState, target.getState(targetPart, targetKey, targetState)); + continue; + } + } + } + } + } +} diff --git a/src/main/java/net/snackbag/vera/style/StyleValue.java b/src/main/java/net/snackbag/vera/style/StyleValue.java new file mode 100644 index 00000000..e39e3ef4 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/StyleValue.java @@ -0,0 +1,4 @@ +package net.snackbag.vera.style; + +public record StyleValue(StyleValueType type, Object value) { +} diff --git a/src/main/java/net/snackbag/vera/style/StyleValueType.java b/src/main/java/net/snackbag/vera/style/StyleValueType.java new file mode 100644 index 00000000..02bfe8f7 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/StyleValueType.java @@ -0,0 +1,125 @@ +package net.snackbag.vera.style; + +import net.minecraft.util.Identifier; +import net.snackbag.mcvera.MinecraftVera; +import net.snackbag.vera.core.*; +import net.snackbag.vera.core.v4.V4Color; +import net.snackbag.vera.core.v4.V4Int; +import net.snackbag.vera.style.animation.easing.VEasings; +import net.snackbag.vera.style.animation.easing.VEasing; +import org.apache.commons.lang3.EnumUtils; +import org.jetbrains.annotations.Nullable; + +public enum StyleValueType { + STRING("", (f, t, e, d) -> d > 0.5 ? t : f), + IDENTIFIER(Identifier.of(MinecraftVera.MOD_ID, "empty"), (f, t, e, d) -> d > 0.5 ? t : f), + INT(0, (from, to, easing, delta) -> easing.apply(from, to, delta)), + FLOAT(0.0F, (from, to, easing, delta) -> easing.apply(from, to, delta)), + + FILL(VFill.empty(), (from, to, easing, delta) -> + from.ease(easing, to, delta)), + COLOR(VColor.black(), (from, to, easing, delta) -> (VColor) from.ease(easing, to, delta)), + FONT(VFont.create(), (from, to, easing, delta) -> + VFont.create().withColor((VColor) from.getColor().ease(easing, to.getColor(), delta)) + .withSize(easing.apply(from.getSize(), to.getSize(), delta)) + .withName(delta > 0.5 ? to.getName() : from.getName())), + CURSOR(VCursorShape.DEFAULT, (f, t, e, d) -> d > 0.5 ? t : f), + EASING(VEasings.getDefault(), (f, t, e, d) -> d > 0.5 ? t : f), + + V4INT(new V4Int(0), (from, to, easing, delta) -> new V4Int( + easing.apply(from.get1(), to.get1(), delta), + easing.apply(from.get2(), to.get2(), delta), + easing.apply(from.get3(), to.get3(), delta), + easing.apply(from.get4(), to.get4(), delta) + )), + V4COLOR(new V4Color(VColor.black()), (from, to, easing, delta) -> new V4Color( + (VColor) from.get1().ease(easing, to.get1(), delta), + (VColor) from.get2().ease(easing, to.get2(), delta), + (VColor) from.get3().ease(easing, to.get3(), delta), + (VColor) from.get4().ease(easing, to.get4(), delta) + )); + + private static final String ID_REGEX = "^[\\w-./]*:[\\w-./]*$"; + + public final Object standard; + public final EaseContext animationTransition; + + StyleValueType(T standard, EaseContext animationTransition) { + this.standard = standard; + this.animationTransition = (EaseContext) animationTransition; + } + + public static StyleValueType get(Object val, @Nullable StyleValueType bias) { + if (val instanceof String s) { + if (bias == IDENTIFIER && s.matches(ID_REGEX)) return IDENTIFIER; + else if (bias == CURSOR && EnumUtils.getEnumIgnoreCase(VCursorShape.class, s) != null) return CURSOR; + else if (bias == EASING && VEasings.getIgnoreCase(s) != null) return EASING; + else if (bias == FILL && s.matches(ID_REGEX)) return FILL; + return STRING; + } else if (val instanceof V4Color || (bias == V4COLOR && (val instanceof VColor[] || val instanceof VColor))) + return V4COLOR; + else if (val instanceof V4Int || (bias == V4INT && (val instanceof int[] || val instanceof Integer[] || val instanceof Integer))) + return V4INT; + else if (val instanceof Identifier) { + if (bias == FILL) return FILL; + return IDENTIFIER; + } + else if (val instanceof VCursorShape) return CURSOR; + else if (val instanceof VEasing) return EASING; + else if (val instanceof Integer) return INT; + else if (val instanceof Float || val instanceof Double) return FLOAT; + else if (val instanceof VColor) { + if (bias == FILL) return FILL; + return COLOR; + } + else if (val instanceof VImage) return FILL; + else if (val instanceof VFont) return FONT; + else throw new RuntimeException("%s isn't a valid style type".formatted(val.getClass().getName())); + } + + public static Object convert(Object value, StyleValueType to) { + if (value instanceof String v) { + if (to == IDENTIFIER) return new Identifier(v); + else if (to == CURSOR) return EnumUtils.getEnumIgnoreCase(VCursorShape.class, v); + else if (to == EASING) return VEasings.getIgnoreCase(v); + else if (to == FILL) return new VImage(new Identifier(v)); + } + + else if (value instanceof Identifier i && to == FILL) return new VImage(i); + + else if (value instanceof int[] || value instanceof Integer[]) { + Integer[] v = (Integer[]) value; + + return switch (v.length) { + case 1 -> new V4Int(v[0]); + case 2 -> new V4Int(v[0], v[1]); + case 4 -> new V4Int(v[0], v[1], v[2], v[3]); + default -> + throw new RuntimeException("invalid V4Int format. Length must be 1, 2 or 4. Provided: %d".formatted(v.length)); + }; + } + + else if (value instanceof Integer v && to == V4INT) return new V4Int(v); + + else if (value instanceof VColor[] v) { + return switch (v.length) { + case 1 -> new V4Color(v[0]); + case 2 -> new V4Color(v[0], v[1]); + case 4 -> new V4Color(v[0], v[1], v[2], v[3]); + default -> + throw new RuntimeException("invalid V4Color format. Length must be 1, 2 or 4. Provided: %d".formatted(v.length)); + }; + } + + else if (value instanceof VColor v && to == V4COLOR) return new V4Color(v); + else if (value instanceof VFill && to == FILL) return value; + + else if (to == FLOAT && value instanceof Double v) return v.floatValue(); + return value; + } + + @FunctionalInterface + public interface EaseContext { + T apply(T in, T out, VEasing easing, float delta); + } +} diff --git a/src/main/java/net/snackbag/vera/style/VStyleSheet.java b/src/main/java/net/snackbag/vera/style/VStyleSheet.java new file mode 100644 index 00000000..1ddc4510 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/VStyleSheet.java @@ -0,0 +1,264 @@ +package net.snackbag.vera.style; + +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VFont; +import net.snackbag.vera.widget.VWidget; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Array; +import java.util.*; + +public class VStyleSheet { + private final StyleContainer> widgetSpecificStyles = new StyleContainer<>(); // like HTML #IDs + private final StyleContainer classStyles = new StyleContainer<>(); // like CSS .classes + private final StyleContainer> standardStyles = new StyleContainer<>(); // like HTML + + private HashMap typeRegistry = new HashMap<>(); + + public @Nullable T getKey(VWidget widget, String key) { + return getKey(widget, key, VStyleState.DEFAULT); + } + + public @Nullable T getKey(VWidget widget, String key, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + + // if widget contains key + if (widgetSpecificStyles.hasKey(widget, key)) { + // if widget has state, return + if (widgetSpecificStyles.hasState(widget, key, state)) return widgetSpecificStyles.getState(widget, key, state); + + // if widget state has fallback, attempt + if (state.fallback != null) return getKey(widget, key, state.fallback); + } + + // if class contains key + HashMap> mixed = mixClasses(widget.classes); + + Contains: if (mixed.containsKey(key)) { + if (!mixed.get(key).containsKey(state)) { + if (state.fallback == null) break Contains; + return getKey(widget, key, state.fallback); + } + return (T) mixed.get(key).get(state); + } + + // if nothing worked, try standard keys or return null + return getStandardKey(widget.getClass(), key, state); + } + + /** + * In this case, stacked means that also all keys from states below the + * given state are returned. + */ + public Set getKeysStacked(VWidget widget, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + + Set keys = new HashSet<>(); + + // standard styles + keys.addAll(standardStyles.getKeysStacked(widget.getClass(), state)); + + // class styles + for (String clazz : widget.classes) { + keys.addAll(classStyles.getKeysStacked(clazz, state)); + } + + // widget specific + keys.addAll(widgetSpecificStyles.getKeysStacked(widget, state)); + + return keys; + } + + /** + * Note: the resolved keys will return keys from states below the given state + */ + public HashMap getResolvedKeys(VWidget widget, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + + Set keys = getKeysStacked(widget, state); + HashMap buffer = new HashMap<>(); + + for (String key : keys) { + buffer.put(key, getKey(widget, key, state)); + } + + return buffer; + } + + /** + * Resolves a standard style key by traversing the class hierarchy and style states. + *

+ * Step by step:
+ * 1. If clazz is null, return null
+ * 2. If clazz is not registered in {@link VStyleSheet#standardStyles}, recurse into its superclass
+ * 3. If the specified key is not defined for this class, recurse into its superclass
+ * 4. Check if the given state is defined:
+ *     - If not, and {@link VStyleState#fallback} exists, retry with the fallback state on the same class
+ *     - If fallback also fails or is null, recurse into the superclass with the original state + */ + public @Nullable T getStandardKey(@Nullable Class clazz, String key, @NotNull VStyleState state) { + if (clazz == null) return null; + + // if class isn't registered, attempt super + if (!standardStyles.hasPart(clazz)) return getStandardKey(clazz.getSuperclass(), key, state); + + // if no key found, attempt super + if (!standardStyles.hasKey(clazz, key)) return getStandardKey(clazz.getSuperclass(), key, state); + + // if no state found, return same class but fallback state + if (!standardStyles.hasState(clazz, key, state)) { + // if fallback state is null, attempt superclass + if (state.fallback == null) return getStandardKey(clazz.getSuperclass(), key, state); + + // try fallback state + return getStandardKey(clazz, key, state.fallback); + } + + return standardStyles.getState(clazz, key, state); + } + + public void setKey(VWidget widget, String key, Object object) { + setKey(widget, key, object, VStyleState.DEFAULT); + } + + public void setKey(String clazz, String key, Object object) { + setKey(clazz, key, object, VStyleState.DEFAULT); + } + + public void setKey(Class clazz, String key, Object object) { + setKey(clazz, key, object, VStyleState.DEFAULT); + } + + public void setKey(VWidget widget, String key, Object value, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + value = potentiallyUnpackArray(value); + + StyleValueType res = getReservation(key); + StyleValueType valRes = StyleValueType.get(value, res); + + if (res != null) { + if (valRes != res) + throw new RuntimeException("Cannot set key %s, because it is reserved for type %s. Received: %s".formatted(key, res, valRes)); + } else reserveType(key, valRes); + + value = StyleValueType.convert(value, valRes); + widgetSpecificStyles.put(widget, key, state, value); + } + + public void setKey(String clazz, String key, Object value, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + value = potentiallyUnpackArray(value); + + StyleValueType res = getReservation(key); + StyleValueType valRes = StyleValueType.get(value, res); + + if (res != null) { + if (valRes != res) + throw new RuntimeException("Cannot set key %s (for class %s), because it is reserved for type %s. Received: %s".formatted(key, clazz, res, valRes)); + } else reserveType(key, valRes); + + value = StyleValueType.convert(value, valRes); + classStyles.put(clazz, key, state, value); + } + + public void setKey(Class clazz, String key, Object value, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + value = potentiallyUnpackArray(value); + + StyleValueType res = getReservation(key); + StyleValueType valRes = StyleValueType.get(value, res); + + if (res != null) { + if (valRes != res) + throw new RuntimeException("Cannot set standard key %s (for class %s), because it is reserved for type %s. Received: %s".formatted(key, clazz, res, valRes)); + } else reserveType(key, valRes); + + value = StyleValueType.convert(value, valRes); + standardStyles.put(clazz, key, state, value); + } + + public VColor.ColorModifier modifyKeyAsColor(VWidget widget, String key) { + return modifyKeyAsColor(widget, key, VStyleState.DEFAULT); + } + + public VColor.ColorModifier modifyKeyAsColor(VWidget widget, String key, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + + @Nullable VStyleState finalState = state; + return new VColor.ColorModifier(getKey(widget, key, state), color -> setKey(widget, key, color, finalState)); + } + + public VFont.FontModifier modifyKeyAsFont(VWidget widget, String key) { + return modifyKeyAsFont(widget, key, VStyleState.DEFAULT); + } + + public VFont.FontModifier modifyKeyAsFont(VWidget widget, String key, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + + @Nullable VStyleState finalState = state; + return new VFont.FontModifier(getKey(widget, key, state), font -> setKey(widget, key, font, finalState)); + } + + public VColor.ColorModifier modifyKeyAsFontColor(VWidget widget, String key) { + return modifyKeyAsFontColor(widget, key, VStyleState.DEFAULT); + } + + public VColor.ColorModifier modifyKeyAsFontColor(VWidget widget, String key, @Nullable VStyleState state) { + if (state == null) state = VStyleState.DEFAULT; + + // this is cursed + @Nullable VStyleState finalState = state; + return new VColor.ColorModifier( + ((VFont) getKey(widget, key, state)).getColor(), + color -> modifyKeyAsFont(widget, key, finalState).color(color) + ); + } + + public void reserveType(String key, StyleValueType type) { // TODO: make safer (aka stop if already reserved or values inside) + typeRegistry.put(key, type); + } + + public @Nullable StyleValueType getReservation(String key) { + return typeRegistry.getOrDefault(key, null); + } + + private Object potentiallyUnpackArray(Object value) { + if (value.getClass().isArray()) { + int length = Array.getLength(value); + if (length == 1) value = Array.get(value, 0); + } + + return value; + } + + public void addSheet(VStyleSheet target) { + // Merge type registries + HashMap mergedTypeRegistry = new HashMap<>(typeRegistry); + + for (String key : target.typeRegistry.keySet()) { + StyleValueType type = target.typeRegistry.get(key); + + if (!mergedTypeRegistry.containsKey(key)) mergedTypeRegistry.put(key, type); + else if (mergedTypeRegistry.get(key) != type) + throw new UnsupportedOperationException("Cannot merge two sheets with different type registry entries. Received %s:%s; already %s".formatted(key, type, mergedTypeRegistry.get(key))); + } + + // Apply changes if everything went fine + typeRegistry = mergedTypeRegistry; + standardStyles.moldWith(target.standardStyles); + classStyles.moldWith(target.classStyles); + widgetSpecificStyles.moldWith(target.widgetSpecificStyles); + } + + public HashMap> mixClasses(LinkedHashSet classes) { + final HashMap> values = new HashMap<>(); + + for (String clazz : classes) { + HashMap> styles = classStyles.getPart(clazz); + for (String key : styles.keySet()) values.put(key, styles.get(key)); + } + + return values; + } +} diff --git a/src/main/java/net/snackbag/vera/style/VStyleState.java b/src/main/java/net/snackbag/vera/style/VStyleState.java new file mode 100644 index 00000000..ecd4e429 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/VStyleState.java @@ -0,0 +1,54 @@ +package net.snackbag.vera.style; + +import org.jetbrains.annotations.Nullable; + +public enum VStyleState { + DEFAULT("default"), + HOVERED("hover", DEFAULT), + + CLICKED("clicked", HOVERED), + LEFT_CLICKED("left-click", CLICKED), + MIDDLE_CLICKED("middle-click", CLICKED), + RIGHT_CLICKED("right-click", CLICKED), + + LC_DRAGGING("lc-drag", LEFT_CLICKED), + LC_DRAG_TOP("lc-drag-top", LC_DRAGGING), + LC_DRAG_BOTTOM("lc-drag-bottom", LC_DRAGGING), + LC_DRAG_LEFT("lc-drag-left", LC_DRAGGING), + LC_DRAG_RIGHT("lc-drag-right", LC_DRAGGING), + + MC_DRAGGING("mc-drag", MIDDLE_CLICKED), + MC_DRAG_TOP("mc-drag-top", MC_DRAGGING), + MC_DRAG_BOTTOM("mc-drag-bottom", MC_DRAGGING), + MC_DRAG_LEFT("mc-drag-left", MC_DRAGGING), + MC_DRAG_RIGHT("mc-drag-right", MC_DRAGGING), + + RC_DRAGGING("rc-drag", RIGHT_CLICKED), + RC_DRAG_TOP("rc-drag-top", RC_DRAGGING), + RC_DRAG_BOTTOM("rc-drag-bottom", RC_DRAGGING), + RC_DRAG_LEFT("rc-drag-left", RC_DRAGGING), + RC_DRAG_RIGHT("rc-drag-right", RC_DRAGGING); + + public final String identifier; + public final @Nullable VStyleState fallback; + + VStyleState(String identifier) { + this(identifier, null); + } + + VStyleState(String identifier, @Nullable VStyleState fallback) { + this.identifier = identifier; + this.fallback = fallback; + } + + public boolean inherits(VStyleState state) { + VStyleState next = this; + + while (next != null) { + if (next == state) return true; + next = next.fallback; + } + + return false; + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/AnimationEngine.java b/src/main/java/net/snackbag/vera/style/animation/AnimationEngine.java new file mode 100644 index 00000000..c9a4cb32 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/AnimationEngine.java @@ -0,0 +1,153 @@ +package net.snackbag.vera.style.animation; + +import net.snackbag.mcvera.MinecraftVera; +import net.snackbag.vera.event.VEvents; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.widget.VWidget; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class AnimationEngine { + public final VWidget widget; + private final HashMap active = new HashMap<>(); + + public AnimationEngine(VWidget widget) { + this.widget = widget; + } + + public void start(VAnimation animation) { + if (active.containsKey(animation.name)) { + MinecraftVera.LOGGER.warn("Couldn't start animation %s, because it's already running".formatted(animation.name)); + return; + } + + CompiledAnimation compiled = animation.compile(widget.getApp(), widget); + active.put(compiled.name, new PlaybackContext(compiled, System.currentTimeMillis())); + widget.events.fire(VEvents.Animation.BEGIN, animation); + } + + public void startOrRewind(VAnimation animation) { + if (active.containsKey(animation.name)) rewind(animation); + else start(animation); + } + + public void stop(VAnimation animation) { + stop(animation.name); + } + + public void stop(String name) { + if (!active.containsKey(name)) { + MinecraftVera.LOGGER.warn("Couldn't stop animation %s, because it isn't active".formatted(name)); + return; + } + + PlaybackContext ctx = active.get(name); + widget.events.fire(VEvents.Animation.FINISH, ctx.animation, ctx.startTime); + active.remove(name); + } + + public void unwind(VAnimation animation) { + unwind(animation.name); + } + + public void unwind(String name) { + if (active.containsKey(name)) { + active.get(name).unwind(); + widget.events.fire(VEvents.Animation.UNWIND_BEGIN, active.get(name).animation); + } + else MinecraftVera.LOGGER.warn("Couldn't unwind %s, because it's not active".formatted(name)); + } + + public void rewind(VAnimation animation) { + rewind(animation.name); + } + + public void rewind(String name) { + if (!active.containsKey(name)) { + MinecraftVera.LOGGER.warn("Couldn't rewind %s, because it's not active".formatted(name)); + return; + } + + PlaybackContext ctx = active.get(name); + if (ctx.getWindingProgress() <= 0.0f) { + MinecraftVera.LOGGER.warn("Couldn't rewind %s, because it's not unwinding".formatted(name)); + return; + } + + ctx.rewind(); + widget.events.fire(VEvents.Animation.REWIND_BEGIN, ctx.animation); + } + + public T animateStyle(String key, T value) { + for (PlaybackContext ctx : active.values()) { + CompiledAnimation animation = ctx.animation; + if (!animation.keys.contains(key)) continue; + + int time = ctx.getRelativeTime(); + + // select active keyframes + int kfIndex = animation.getKeyframeIndexAtTime(time); + int fromKfIndex = Math.max(kfIndex - 1, 0); + boolean loopSpoofed = false; + + if (ctx.getCurrentLoopNumber() > 1) { // handle loop mode; spoof first keyframe with last one for smooth transition + if (fromKfIndex == 0) { + fromKfIndex = animation.keyframes.size() - 1; + loopSpoofed = true; + } + } + + VKeyframe to = animation.keyframes.get(kfIndex); + VKeyframe from = animation.keyframes.get(fromKfIndex); + + int fromKfWhen = animation.getWhenKeyframe(from); + if (ctx.getCurrentLoopNumber() > 1 && loopSpoofed) { // handle loop mode; spoof beginning time for smooth transition + fromKfWhen = 0; + } + + float delta = animation.getKeyframeDelta(time, fromKfWhen, from, to); + + StyleValueType reservation = widget.getApp().styleSheet.getReservation(key); + T kfEase = (T) reservation.animationTransition.apply( // ease keyframe transition + from.styles.get(key), to.styles.get(key), + to.easing, delta); + T windingEase = (T) reservation.animationTransition.apply( // ease winding + kfEase, value, + animation.unwindEasing, ctx.getWindingProgress() + ); + return windingEase; + } + + return value; + } + + public void updateLifetimes() { + HashMap copies = new HashMap<>(active); // fix concurrency crash + + for (Map.Entry entry : copies.entrySet()) { + PlaybackContext ctx = entry.getValue(); + String name = entry.getKey(); + CompiledAnimation animation = ctx.animation; + + ctx.potentiallyResetWinding(); + + if (animation.unwindAtEnd && animation.loopMode == VLoopMode.NONE && ctx.getProgress() >= 1.0f) { + unwind(name); + } + + if (ctx.getWindingProgress() >= 1.0f || (!animation.unwindAtEnd && ctx.getProgress() >= 1.0f && (ctx.getWindingProgress() == 0.0f || ctx.getWindingProgress() == 1.0f))) { + stop(name); + } + } + } + + public boolean isActive(String animation) { + return active.containsKey(animation.toLowerCase()); + } + + public Set getActive() { + return active.keySet(); + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/CompiledAnimation.java b/src/main/java/net/snackbag/vera/style/animation/CompiledAnimation.java new file mode 100644 index 00000000..2896c111 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/CompiledAnimation.java @@ -0,0 +1,110 @@ +package net.snackbag.vera.style.animation; + +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.style.animation.easing.VEasing; +import net.snackbag.vera.widget.VWidget; + +import java.util.List; + +/** + * Version of the animation class that is immutable and has additional values: {@link #app}, {@link #duration} and + * {@link #keys}. The {@link #keyframes} variable has also been altered drastically. + */ +public class CompiledAnimation { + /** + * Assigned app, used for stylesheets + */ + public final VeraApp app; + + public final String name; + public final VLoopMode loopMode; + public final VEasing unwindEasing; + public final int unwindTime; + public final boolean unwindAtEnd; + + /** + * Sum of all keyframes's transition time + stay time + */ + public final int duration; + /** + * List of all keyframes. The difference to the normal {@link VAnimation} object is that each keyframe has the + * style keys of each keyframe in the entire animation, even if unchanged. + */ + public final List keyframes; + /** + * All keys that are affected by the animation. Used in caching to calculate animation styles. + */ + public final List keys; + + /** + * Does not compile anything by itself, it simply accepts precompiled values. Compilation occurs in + * {@link VAnimation#compile(VeraApp, VWidget)} + */ + protected CompiledAnimation( + VeraApp app, + String name, int duration, + VLoopMode loopMode, + VEasing unwindEasing, int unwindTime, boolean unwindAtEnd, + List keyframes, List keys + ) { + this.app = app; + + this.name = name; + this.duration = duration; + this.loopMode = loopMode; + this.unwindEasing = unwindEasing; + this.unwindTime = unwindTime; + this.unwindAtEnd = unwindAtEnd; + this.keyframes = keyframes; + this.keys = keys; + } + + @Override + public String toString() { + return "CompiledAnimation{name='%s', duration='%s', loopMode='%s' unwindEasing='%s', unwindTime=%s, unwindAtEnd=%s, keyframeCount=%s}" + .formatted(name, duration, loopMode, unwindEasing, unwindTime, unwindAtEnd, keyframes.size()); + } + + public int getKeyframeIndexAtTime(int time) { + int bufferTime = 0; + + for (int i = 0; i < keyframes.size(); i++) { + VKeyframe keyframe = keyframes.get(i); + + if (keyframe.transitionTime <= 0 && keyframe.stayTime <= 0) continue; + if (time > bufferTime && time < bufferTime + keyframe.transitionTime + keyframe.stayTime) { + return i; + } + + bufferTime += keyframe.transitionTime + keyframe.stayTime; + } + + return keyframes.size() - 1; + } + + public int getWhenKeyframe(VKeyframe keyframe) { + int buffer = 0; + + for (VKeyframe frame : keyframes) { + if (frame == keyframe) break; + buffer += frame.transitionTime + frame.stayTime; + } + + return buffer; + } + + public float getKeyframeDelta(int time, VKeyframe from, VKeyframe to) { + return getKeyframeDelta(time, getWhenKeyframe(from), from, to); + } + + public float getKeyframeDelta(int time, int whenFrom, VKeyframe from, VKeyframe to) { + int fromTime = whenFrom + from.transitionTime + from.stayTime; + int toTime = fromTime + to.transitionTime; + + if (time > toTime) return 1f; + else if (time > fromTime) return (float) (time - fromTime) / (toTime - fromTime); // time <= toTime already true + + // time < fromTime + return 0f; + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/PlaybackContext.java b/src/main/java/net/snackbag/vera/style/animation/PlaybackContext.java new file mode 100644 index 00000000..2ef35253 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/PlaybackContext.java @@ -0,0 +1,76 @@ +package net.snackbag.vera.style.animation; + +import net.minecraft.util.math.MathHelper; + +public class PlaybackContext { + public final CompiledAnimation animation; + public final long startTime; + + private float prevWindingProgress = 0f; + private long windingStartTime = -1; + private int unwindStartRelativeTime = -1; // for the relative unwinding + private boolean unwindingOrRewinding = false; // unwinding = false; rewinding = true + + public PlaybackContext(CompiledAnimation animation, long startTime) { + this.animation = animation; + this.startTime = startTime; + } + + public int getRelativeTime() { + int relative = Math.toIntExact(System.currentTimeMillis() - startTime); + + return switch (animation.loopMode) { + case NONE -> relative; + case FORWARD_REPEAT -> relative % animation.duration; + }; + } + + public float getProgress() { + return Math.min((float) getRelativeTime() / animation.duration, 1.0f); + } + + public int getCurrentLoopNumber() { + return (int) Math.ceil((float) (System.currentTimeMillis() - startTime) / animation.duration); + } + + /** + * 0 = animation has full impact, 1 = animation has no impact + * @return the current winding progress from 0 to 1 + */ + public float getWindingProgress() { + if (windingStartTime == -1) return 0f; + + int unwindTime = animation.unwindTime; + if (unwindTime == -1) unwindTime = unwindStartRelativeTime; + + float delta = (float) (System.currentTimeMillis() - windingStartTime) / unwindTime; + if (unwindingOrRewinding) return MathHelper.clamp(prevWindingProgress - delta, 0f, 1f); // is rewinding + else return MathHelper.clamp(prevWindingProgress + delta, 0f, 1f); // is unwinding + } + + public void unwind() { + if (!unwindingOrRewinding && windingStartTime != -1) return; // if already unwinding + if (windingStartTime == -1) unwindStartRelativeTime = getRelativeTime(); + + prevWindingProgress = getWindingProgress(); + windingStartTime = System.currentTimeMillis() - 1; + unwindingOrRewinding = false; + } + + public void rewind() { + if (windingStartTime == -1 || unwindingOrRewinding) return; // if not unwinding OR if already rewinding + + prevWindingProgress = getWindingProgress(); + windingStartTime = System.currentTimeMillis(); + unwindingOrRewinding = true; + } + + public void potentiallyResetWinding() { + if (getWindingProgress() > 0.0f) return; + + prevWindingProgress = 0f; + windingStartTime = -1; + unwindStartRelativeTime = -1; + unwindingOrRewinding = false; + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/VAnimation.java b/src/main/java/net/snackbag/vera/style/animation/VAnimation.java new file mode 100644 index 00000000..e0beecb2 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/VAnimation.java @@ -0,0 +1,154 @@ +package net.snackbag.vera.style.animation; + +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.style.StyleValue; +import net.snackbag.vera.style.animation.easing.VEasings; +import net.snackbag.vera.style.animation.easing.VEasing; +import net.snackbag.vera.widget.VWidget; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.function.Consumer; + +public class VAnimation { + public static final String INTERNAL_TRANSITION_NAME = "internal-vera-transition"; + public String name; + public VLoopMode loopMode; + public List keyframes; + + public VEasing unwindEasing; + public int unwindTime; + public boolean unwindAtEnd; + + public VAnimation(String name, VLoopMode loopMode, VEasing unwindEasing, int unwindTime, boolean unwindAtEnd, List keyframes) { + this.name = name.toLowerCase(); + this.loopMode = loopMode; + this.keyframes = keyframes; + this.unwindEasing = unwindEasing; + this.unwindTime = unwindTime; + this.unwindAtEnd = unwindAtEnd; + } + + public CompiledAnimation compile(VeraApp app, VWidget widget) { + VKeyframe.ListIndex index = VKeyframe.createIndex(keyframes, app.styleSheet); + + List extendedFrames = new ArrayList<>(); + List loopingFrames = new ArrayList<>(keyframes); + HashMap styleMemory = new HashMap<>(); + + loopingFrames.add(0, new VKeyframe(0, 0, VEasings.IMMEDIATE)); + + for (VKeyframe original : loopingFrames) { + VKeyframe frame = new VKeyframe(original); // copy keyframe + HashMap frameStyles = index.keyframeValueMap.get(original); // get all registered styles + + // populate all styles & memorize + for (String style : index.styles) { + if (frame.styles.containsKey(style)) { + // skip & remove keys that weren't reserved when the index was created; fixes NPE + if (frameStyles.get(style) == null || app.styleSheet.getKey(widget, style) == null) { + frame.styles.remove(style); + continue; + } + + styleMemory.put(style, frameStyles.get(style)); + continue; + } + + // set default style + if (!styleMemory.containsKey(style)) { + styleMemory.put(style, new StyleValue( + app.styleSheet.getReservation(style), + app.styleSheet.getKey(widget, style)) + ); + } + frame.style(style, styleMemory.get(style).value()); // actual populating + } + + extendedFrames.add(frame); + } + + if (loopMode == VLoopMode.FORWARD_REPEAT) { // add immediate end for smooth transition + VKeyframe frame = new VKeyframe( + keyframes.get(keyframes.size() - 1), + 0, + 0, + VEasings.IMMEDIATE + ); + extendedFrames.add(frame); + } + + return new CompiledAnimation( + app, + name, index.totalDuration, + loopMode, + unwindEasing, unwindTime, unwindAtEnd, + extendedFrames, index.styles + ); + } + + @Override + public String toString() { + return "VAnimation{name='%s'}".formatted(name); + } + + public static class Builder { + private final String name; + private final List keyframes = new ArrayList<>(); + + private VLoopMode loopMode = VLoopMode.NONE; + private VEasing unwindEasing = VEasings.getDefault(); + private int unwindTime = 0; + private boolean unwindAtEnd = false; + + public Builder(String name) { + this.name = name; + } + + public Builder keyframe(int stayMs, Consumer apply) { + return keyframe(0, stayMs, apply, VEasings.getDefault()); + } + + public Builder keyframe(int transitionMs, int stayMs, Consumer apply) { + return keyframe(transitionMs, stayMs, apply, VEasings.getDefault()); + } + + public Builder keyframe(int transitionMs, int stayMs, Consumer apply, VEasing easing) { + VKeyframe frame = new VKeyframe(stayMs, transitionMs, easing); + apply.accept(frame); + + keyframes.add(frame); + return this; + } + + public Builder loopMode(VLoopMode mode) { + this.loopMode = mode; + return this; + } + + public Builder unwindEasing(VEasing easing) { + this.unwindEasing = easing; + return this; + } + + public Builder unwindTime(int ms) { + this.unwindTime = ms; + return this; + } + + public Builder relativeUnwindTime() { + this.unwindTime = -1; + return this; + } + + public Builder unwindAtEnd() { + this.unwindAtEnd = true; + return this; + } + + public VAnimation build() { + return new VAnimation(name, loopMode, unwindEasing, unwindTime, unwindAtEnd, keyframes); + } + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/VKeyframe.java b/src/main/java/net/snackbag/vera/style/animation/VKeyframe.java new file mode 100644 index 00000000..25e0468c --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/VKeyframe.java @@ -0,0 +1,107 @@ +package net.snackbag.vera.style.animation; + +import net.snackbag.mcvera.MinecraftVera; +import net.snackbag.vera.style.StyleValue; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.style.animation.easing.VEasing; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class VKeyframe { + protected final int stayTime; + protected final int transitionTime; + protected final VEasing easing; + + protected final HashMap styles = new HashMap<>(); + + public VKeyframe(int stayTime, int transitionTime, VEasing easing) { + this.stayTime = stayTime; + this.transitionTime = transitionTime; + this.easing = easing; + } + + public VKeyframe(VKeyframe other) { + this(other, other.transitionTime, other.stayTime, other.easing); + } + + public VKeyframe(VKeyframe other, int transitionTime, int stayTime, VEasing easing) { + this.transitionTime = transitionTime; + this.stayTime = stayTime; + this.easing = easing; + + styles.putAll(other.styles); + } + + public void style(String key, Object value) { + if (styles.containsKey(key)) throw new RuntimeException("Key '%s' cannot be reassigned within the same keyframe".formatted(key)); + styles.put(key, value); + } + + public String dump() { + StringBuilder sb = new StringBuilder(); + sb.append("transitionTime=").append(transitionTime).append('\n'); + sb.append("stayTime=").append(stayTime).append('\n'); + sb.append("easing=").append(easing).append('\n'); + for (var e : styles.entrySet()) { + sb.append(e.getKey()).append(" = ").append(e.getValue()).append('\n'); + } + return sb.toString(); + } + + public static class ListIndex { + public final List styles = new ArrayList<>(); + public final LinkedHashMap> keyframeValueMap = new LinkedHashMap<>(); + public final int totalDuration; + + /** + * Gathers styles and total duration of a given list of keyframes. + * + * @param keyframes the list of keyframes + * @param sheet the stylesheet to check keyframe style registration. If null, check will be omitted + */ + private ListIndex(List keyframes, @Nullable VStyleSheet sheet) { + int dur = 0; + + // counting + for (VKeyframe frame : keyframes) { + HashMap valueMap = new HashMap<>(); + dur += frame.transitionTime + frame.stayTime; + + for (Map.Entry entry : frame.styles.entrySet()) { + String key = entry.getKey(); + Object val = entry.getValue(); + + if (!styles.contains(key)) styles.add(key); + + SheetCheck: if (sheet != null) { + StyleValueType res = sheet.getReservation(key); + if (res == null) { + MinecraftVera.LOGGER.warn("Cannot set keyframe style to unreserved style key: %s. Key removed.".formatted(key)); + styles.remove(key); + frame.styles.remove(key); + break SheetCheck; + } + + StyleValueType testRes = StyleValueType.get(val, res); + if (testRes != res) throw new RuntimeException( + "Cannot create keyframe with key '%s', because it has an incorrect value type. Got: %s, require %s" + .formatted(key, testRes, res)); + + valueMap.put(key, new StyleValue(res, val)); + } + + keyframeValueMap.put(frame, valueMap); + } + } + + // apply values + totalDuration = dur; + } + } + + public static ListIndex createIndex(List keyframes, @Nullable VStyleSheet sheet) { + return new ListIndex(keyframes, sheet); + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/VLoopMode.java b/src/main/java/net/snackbag/vera/style/animation/VLoopMode.java new file mode 100644 index 00000000..24ee087b --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/VLoopMode.java @@ -0,0 +1,8 @@ +package net.snackbag.vera.style.animation; + +public enum VLoopMode { + NONE, + FORWARD_REPEAT, + + // TODO: reverse repeat +} diff --git a/src/main/java/net/snackbag/vera/style/animation/easing/ImmediateEasing.java b/src/main/java/net/snackbag/vera/style/animation/easing/ImmediateEasing.java new file mode 100644 index 00000000..65e4867c --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/easing/ImmediateEasing.java @@ -0,0 +1,17 @@ +package net.snackbag.vera.style.animation.easing; + +public class ImmediateEasing extends VEasing { + public ImmediateEasing() { + super("immediate"); + } + + @Override + public float apply(float from, float to, float delta) { + return to; + } + + @Override + public int apply(int from, int to, float delta) { + return to; + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/easing/LinearEasing.java b/src/main/java/net/snackbag/vera/style/animation/easing/LinearEasing.java new file mode 100644 index 00000000..e728b094 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/easing/LinearEasing.java @@ -0,0 +1,12 @@ +package net.snackbag.vera.style.animation.easing; + +public class LinearEasing extends VEasing { + protected LinearEasing() { + super("linear"); + } + + @Override + public float apply(float from, float to, float delta) { + return from + delta * (to - from); + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/easing/VEasing.java b/src/main/java/net/snackbag/vera/style/animation/easing/VEasing.java new file mode 100644 index 00000000..b8025176 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/easing/VEasing.java @@ -0,0 +1,15 @@ +package net.snackbag.vera.style.animation.easing; + +import net.snackbag.vera.Vera; + +public abstract class VEasing { + public VEasing(String name) { + if (Vera.registrar.getEasingIgnoreCase(name) != null) return; + Vera.registrar.registerEasing(name, this); + } + + public abstract float apply(float from, float to, float delta); + public int apply(int from, int to, float delta) { + return Math.round(apply((float) from, (float) to, delta)); + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/easing/VEasings.java b/src/main/java/net/snackbag/vera/style/animation/easing/VEasings.java new file mode 100644 index 00000000..156b19ac --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/easing/VEasings.java @@ -0,0 +1,16 @@ +package net.snackbag.vera.style.animation.easing; + +import net.snackbag.vera.Vera; + +public class VEasings { + public static final ImmediateEasing IMMEDIATE = new ImmediateEasing(); + public static final LinearEasing LINEAR = new LinearEasing(); + + public static VEasing getDefault() { + return LINEAR; + } + + public static VEasing getIgnoreCase(String name) { + return Vera.registrar.getEasingIgnoreCase(name); + } +} diff --git a/src/main/java/net/snackbag/vera/style/standard/CheckBoxStandardStyle.java b/src/main/java/net/snackbag/vera/style/standard/CheckBoxStandardStyle.java new file mode 100644 index 00000000..26971e74 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/CheckBoxStandardStyle.java @@ -0,0 +1,25 @@ +package net.snackbag.vera.style.standard; + +import net.minecraft.util.Identifier; +import net.snackbag.mcvera.MinecraftVera; +import net.snackbag.vera.core.VCursorShape; +import net.snackbag.vera.core.VImage; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.widget.VCheckBox; + +public class CheckBoxStandardStyle implements VStandardStyle { + @Override + public void apply(VStyleSheet sheet) { + sheet.setKey(VCheckBox.class, "cursor", VCursorShape.POINTING_HAND, VStyleState.HOVERED); + sheet.setKey(VCheckBox.class, "fill", new VImage(new Identifier(MinecraftVera.MOD_ID, "widgets/checkmark/default.png"))); + sheet.setKey(VCheckBox.class, "fill-checked", new VImage(new Identifier(MinecraftVera.MOD_ID, "widgets/checkmark/checked.png"))); + } + + @Override + public void reserve(VStyleSheet sheet) { + sheet.reserveType("fill", StyleValueType.FILL); + sheet.reserveType("fill-checked", StyleValueType.FILL); + } +} diff --git a/src/main/java/net/snackbag/vera/style/standard/DropdownStandardStyle.java b/src/main/java/net/snackbag/vera/style/standard/DropdownStandardStyle.java new file mode 100644 index 00000000..b584a7af --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/DropdownStandardStyle.java @@ -0,0 +1,27 @@ +package net.snackbag.vera.style.standard; + +import net.snackbag.vera.core.v4.V4Int; +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VCursorShape; +import net.snackbag.vera.core.VFont; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.widget.VDropdown; + +public class DropdownStandardStyle implements VStandardStyle { + @Override + public void apply(VStyleSheet sheet) { + sheet.setKey(VDropdown.class, "background-color", VColor.white()); + sheet.setKey(VDropdown.class, "cursor", VCursorShape.POINTING_HAND, VStyleState.HOVERED); + sheet.setKey(VDropdown.class, "font", VFont.create()); + sheet.setKey(VDropdown.class, "padding", new V4Int(5, 10)); + } + + @Override + public void reserve(VStyleSheet sheet) { + sheet.reserveType("background-color", StyleValueType.COLOR); + sheet.reserveType("font", StyleValueType.FONT); + sheet.reserveType("padding", StyleValueType.V4INT); + } +} diff --git a/src/main/java/net/snackbag/vera/style/standard/LabelStandardStyle.java b/src/main/java/net/snackbag/vera/style/standard/LabelStandardStyle.java new file mode 100644 index 00000000..4d8b66c1 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/LabelStandardStyle.java @@ -0,0 +1,24 @@ +package net.snackbag.vera.style.standard; + +import net.snackbag.vera.core.v4.V4Int; +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VFont; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.widget.VLabel; + +public class LabelStandardStyle implements VStandardStyle { + @Override + public void apply(VStyleSheet sheet) { + sheet.setKey(VLabel.class, "background", VColor.transparent()); + sheet.setKey(VLabel.class, "font", VFont.create()); + sheet.setKey(VLabel.class, "padding", new V4Int(0)); + } + + @Override + public void reserve(VStyleSheet sheet) { + sheet.reserveType("background", StyleValueType.FILL); + sheet.reserveType("font", StyleValueType.FONT); + sheet.reserveType("padding", StyleValueType.V4INT); + } +} diff --git a/src/main/java/net/snackbag/vera/style/standard/LineInputStandardStyle.java b/src/main/java/net/snackbag/vera/style/standard/LineInputStandardStyle.java new file mode 100644 index 00000000..71770b66 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/LineInputStandardStyle.java @@ -0,0 +1,31 @@ +package net.snackbag.vera.style.standard; + +import net.snackbag.vera.core.v4.V4Int; +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VCursorShape; +import net.snackbag.vera.core.VFont; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.widget.VLineInput; + +public class LineInputStandardStyle implements VStandardStyle { + @Override + public void apply(VStyleSheet sheet) { + sheet.setKey(VLineInput.class, "select", VColor.of(0, 120, 215, 0.2f)); + sheet.setKey(VLineInput.class, "background", VColor.white()); + sheet.setKey(VLineInput.class, "cursor", VCursorShape.TEXT, VStyleState.HOVERED); + sheet.setKey(VLineInput.class, "font", VFont.create()); + sheet.setKey(VLineInput.class, "placeholder-font", VFont.create().withColor(VColor.black().withOpacity(0.5f))); + sheet.setKey(VLineInput.class, "padding", new V4Int(4)); + } + + @Override + public void reserve(VStyleSheet sheet) { + sheet.reserveType("select", StyleValueType.FILL); + sheet.reserveType("background", StyleValueType.FILL); + sheet.reserveType("font", StyleValueType.FONT); + sheet.reserveType("placeholder-font", StyleValueType.FONT); + sheet.reserveType("padding", StyleValueType.V4INT); + } +} diff --git a/src/main/java/net/snackbag/vera/style/standard/RectStandardStyle.java b/src/main/java/net/snackbag/vera/style/standard/RectStandardStyle.java new file mode 100644 index 00000000..f24d8d64 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/RectStandardStyle.java @@ -0,0 +1,18 @@ +package net.snackbag.vera.style.standard; + +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.widget.VRect; + +public class RectStandardStyle implements VStandardStyle { + @Override + public void apply(VStyleSheet sheet) { + sheet.setKey(VRect.class, "background", VColor.black()); + } + + @Override + public void reserve(VStyleSheet sheet) { + sheet.reserveType("background", StyleValueType.FILL); + } +} diff --git a/src/main/java/net/snackbag/vera/style/standard/TabWidgetStandardStyle.java b/src/main/java/net/snackbag/vera/style/standard/TabWidgetStandardStyle.java new file mode 100644 index 00000000..1a342e54 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/TabWidgetStandardStyle.java @@ -0,0 +1,32 @@ +package net.snackbag.vera.style.standard; + +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VCursorShape; +import net.snackbag.vera.core.VFont; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.widget.VTabWidget; + +public class TabWidgetStandardStyle implements VStandardStyle { + @Override + public void apply(VStyleSheet sheet) { + sheet.setKey(VTabWidget.class, "background-color", VColor.white()); + sheet.setKey(VTabWidget.class, "background-color-selected", VColor.white().sub(40)); + sheet.setKey(VTabWidget.class, "font", VFont.create()); + sheet.setKey(VTabWidget.class, "cursor", VCursorShape.POINTING_HAND, VStyleState.HOVERED); + + sheet.setKey(VTabWidget.class, "item-spacing-left", 4); + sheet.setKey(VTabWidget.class, "item-spacing-right", 4); + } + + @Override + public void reserve(VStyleSheet sheet) { + sheet.reserveType("font", StyleValueType.FONT); + sheet.reserveType("background-color", StyleValueType.COLOR); + sheet.reserveType("background-color-selected", StyleValueType.COLOR); + + sheet.reserveType("item-spacing-left", StyleValueType.INT); + sheet.reserveType("item-spacing-right", StyleValueType.INT); + } +} diff --git a/src/main/java/net/snackbag/vera/style/standard/VStandardStyle.java b/src/main/java/net/snackbag/vera/style/standard/VStandardStyle.java new file mode 100644 index 00000000..2e9d8f2f --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/VStandardStyle.java @@ -0,0 +1,8 @@ +package net.snackbag.vera.style.standard; + +import net.snackbag.vera.style.VStyleSheet; + +public interface VStandardStyle { + void apply(VStyleSheet sheet); + default void reserve(VStyleSheet sheet) {} +} diff --git a/src/main/java/net/snackbag/vera/style/standard/WidgetStandardStyle.java b/src/main/java/net/snackbag/vera/style/standard/WidgetStandardStyle.java new file mode 100644 index 00000000..a8d0974b --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/WidgetStandardStyle.java @@ -0,0 +1,48 @@ +package net.snackbag.vera.style.standard; + +import net.snackbag.vera.core.v4.V4Color; +import net.snackbag.vera.core.v4.V4Int; +import net.snackbag.vera.core.VColor; +import net.snackbag.vera.core.VCursorShape; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.style.animation.easing.VEasings; +import net.snackbag.vera.widget.VWidget; + +public class WidgetStandardStyle implements VStandardStyle { + @Override + public void apply(VStyleSheet sheet) { + sheet.setKey(VWidget.class, "overlay", VColor.transparent()); + sheet.setKey(VWidget.class, "cursor", VCursorShape.DEFAULT); + + // Border + sheet.setKey(VWidget.class, "border-color", new V4Color(VColor.black())); + sheet.setKey(VWidget.class, "border-size", new V4Int(0)); + + // Transition + sheet.setKey(VWidget.class, "transition", 0); + sheet.setKey(VWidget.class, "transition-easing", VEasings.getDefault()); + + // Rendering + sheet.setKey(VWidget.class, "scale", 1.0f); + sheet.setKey(VWidget.class, "rotation", 0.0f); + } + + @Override + public void reserve(VStyleSheet sheet) { + sheet.reserveType("overlay", StyleValueType.COLOR); + sheet.reserveType("cursor", StyleValueType.CURSOR); + + sheet.reserveType("border-color", StyleValueType.V4COLOR); + sheet.reserveType("border-size", StyleValueType.V4INT); + + sheet.reserveType("transition", StyleValueType.INT); + sheet.reserveType("transition-easing", StyleValueType.EASING); + + sheet.reserveType("scale", StyleValueType.FLOAT); + sheet.reserveType("rotation", StyleValueType.FLOAT); + + // TODO: add background-color + // TODO: add padding + } +} diff --git a/src/main/java/net/snackbag/vera/util/DragHandler.java b/src/main/java/net/snackbag/vera/util/DragHandler.java new file mode 100644 index 00000000..cab5444c --- /dev/null +++ b/src/main/java/net/snackbag/vera/util/DragHandler.java @@ -0,0 +1,84 @@ +package net.snackbag.vera.util; + +import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VMouseButton; +import net.snackbag.vera.event.VEvents; +import net.snackbag.vera.event.VMouseDragEvent; +import net.snackbag.vera.widget.VWidget; +import org.joml.Vector2i; + +public class DragHandler { + public static VMouseButton button = null; + public static VWidget target = null; + public static Vector2i beginPos = null; + + public static VMouseDragEvent.Context prevContext = null; + + public static boolean isDragging() { + return button != null && target != null; + } + + public static void down(VMouseButton button, VWidget target) { + if (isDragging()) return; + + DragHandler.button = button; + DragHandler.target = target; + DragHandler.beginPos = new Vector2i(Vera.getMouseX(), Vera.getMouseY()); + + fireEvents(); + } + + public static void move() { + if (!isDragging()) return; + + fireEvents(); + } + + public static void release(VMouseButton button) { + if (!isDragging()) return; + if (button != DragHandler.button) return; + + clear(); + } + + public static void clear() { + DragHandler.target = null; + DragHandler.button = null; + DragHandler.beginPos = null; + DragHandler.prevContext = null; + } + + public static VMouseDragEvent.Context createContext() { + if (!isDragging()) throw new UnsupportedOperationException("Cannot create mouse drag context when not dragging"); + + int x = Vera.getMouseX(); + int y = Vera.getMouseY(); + + Vector2i move = createMove(); + VMouseDragEvent.Context ctx = new VMouseDragEvent.Context( + beginPos.x, beginPos.y, + x, y, + move.x, move.y, + VMouseDragEvent.Direction.UP + ); + + prevContext = ctx; + return ctx; + } + + private static Vector2i createMove() { + if (prevContext == null) return new Vector2i(0, 0); + else return new Vector2i( + Vera.getMouseX() - prevContext.currentX(), + Vera.getMouseY() - prevContext.currentY() + ); + } + + private static void fireEvents() { + switch (button) { + case LEFT -> target.events.fire(VEvents.Widget.DRAG_LEFT_CLICK, createContext()); + case MIDDLE -> target.events.fire(VEvents.Widget.DRAG_MIDDLE_CLICK, createContext()); + case RIGHT -> target.events.fire(VEvents.Widget.DRAG_RIGHT_CLICK, createContext()); + } + } +} diff --git a/src/main/java/net/snackbag/vera/util/VGeometry.java b/src/main/java/net/snackbag/vera/util/VGeometry.java new file mode 100644 index 00000000..5d592f21 --- /dev/null +++ b/src/main/java/net/snackbag/vera/util/VGeometry.java @@ -0,0 +1,20 @@ +package net.snackbag.vera.util; + +public class VGeometry { + /** + * Method to check whether a coordinate in within the boundaries of a box + * + * @param x check x coord + * @param y check y coord + * @param wx box x coord + * @param wy box y coord + * @param wwidth box width + * @param wheight box height + * + * @return whether param x and y are in the specified box boundaries + */ + public static boolean isInBox(int x, int y, int wx, int wy, int wwidth, int wheight) { + return x >= wx && x <= wx + wwidth && + y >= wy && y <= wy + wheight; + } +} diff --git a/src/main/java/net/snackbag/vera/util/VOnce.java b/src/main/java/net/snackbag/vera/util/VOnce.java new file mode 100644 index 00000000..e059b551 --- /dev/null +++ b/src/main/java/net/snackbag/vera/util/VOnce.java @@ -0,0 +1,70 @@ +package net.snackbag.vera.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class VOnce { + @Nullable + private T value = null; + + /** + * Sets the value if not already set + * + * @param value the value to set + * @throws UnsupportedOperationException if value is already set + * @throws NullPointerException if given value is null + */ + public void set(@NotNull T value) { + Objects.requireNonNull(value, "value must not be null"); + + if (this.value != null) throw new UnsupportedOperationException("Cannot set value that is already set"); + this.value = value; + } + + /** + * Sets the value if not already set without throwing exceptions + * + * @param value the value to set + * @return true if value could be set, false if unable to set + */ + public boolean setSafe(T value) { + if (value == null) return false; + if (this.value != null) return false; + + this.value = value; + return true; + } + + /** + * Checks whether the value is set or not + * + * @return if the value is set + */ + public boolean isSet() { + return value != null; + } + + /** + * Gets the set value if not null + * + * @return the value + * @throws NullPointerException if value is null + */ + public @NotNull T get() { + if (value == null) throw new NullPointerException("Cannot get value that isn't set"); + return value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof VOnce once)) return false; + return Objects.equals(value, once.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/src/main/java/net/snackbag/vera/widget/VCheckBox.java b/src/main/java/net/snackbag/vera/widget/VCheckBox.java index bf5bc6f0..5f3baa40 100644 --- a/src/main/java/net/snackbag/vera/widget/VCheckBox.java +++ b/src/main/java/net/snackbag/vera/widget/VCheckBox.java @@ -1,130 +1,37 @@ package net.snackbag.vera.widget; -import net.minecraft.util.Identifier; -import net.snackbag.mcvera.MinecraftVera; import net.snackbag.vera.Vera; -import net.snackbag.vera.core.VColor; -import net.snackbag.vera.core.VCursorShape; -import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.core.*; +import net.snackbag.vera.event.VEvents; import net.snackbag.vera.event.VCheckedStateChange; -import org.jetbrains.annotations.Nullable; +import net.snackbag.vera.style.VStyleState; public class VCheckBox extends VWidget { private boolean checked; - private Identifier checkedTexture; - private Identifier defaultTexture; - private VColor hoverOverlayColor; - - private @Nullable Identifier checkedHoverTexture; - private @Nullable Identifier defaultHoverTexture; - - public VCheckBox(VeraApp app) { - super(0, 0, 15, 15, app); - - this.checked = false; - - this.checkedTexture = new Identifier(MinecraftVera.MOD_ID, "widgets/checkmark/checked.png"); - this.defaultTexture = new Identifier(MinecraftVera.MOD_ID, "widgets/checkmark/default.png"); - this.hoverOverlayColor = VColor.transparent(); - - setHoverCursor(VCursorShape.POINTING_HAND); - } - - public VCheckBox(VeraApp app, Identifier defaultTexture, Identifier checkedTexture) { - this(app); - - this.defaultTexture = defaultTexture; - this.checkedTexture = checkedTexture; - } - - public VCheckBox(VeraApp app, Identifier defaultTexture, Identifier checkedTexture, int width, int height) { - this(app, defaultTexture, checkedTexture); - - setSize(width, height); - } - - public VCheckBox( - VeraApp app, - Identifier defaultTexture, @Nullable Identifier defaultHoverTexture, - Identifier checkedTexture, @Nullable Identifier checkedHoverTexture) { - this(app, defaultTexture, checkedTexture); - - this.defaultHoverTexture = defaultHoverTexture; - this.checkedHoverTexture = checkedHoverTexture; + public VCheckBox(VAppAccess app) { + this(app, 15, 15); } - public VCheckBox( - VeraApp app, - Identifier defaultTexture, @Nullable Identifier defaultHoverTexture, - Identifier checkedTexture, @Nullable Identifier checkedHoverTexture, - int width, int height) { - this(app, defaultTexture, defaultHoverTexture, checkedTexture, checkedHoverTexture); + public VCheckBox(VAppAccess app, int width, int height) { + super(0, 0, width, height, app); - setSize(width, height); + this.checked = false; } @Override - public void render() { - Vera.renderer.drawImage(app, x, y, width, height, 0, getCurrentTexture()); - if (isHovered()) Vera.renderer.drawRect(app, x, y, width, height, 0, hoverOverlayColor); + public void renderContent(VRenderContext ctx) { + VStyleState state = createStyleState(); + VFill fill = checked ? getStyle("fill-checked", state) : getStyle("fill", state); + + Vera.renderer.drawFill(ctx, 0, 0, width, height, fill); } @Override public void handleBuiltinEvent(String event, Object... args) { - if (event.equals("left-click")) setChecked(!checked); super.handleBuiltinEvent(event, args); - } - - public Identifier getCurrentTexture() { - if (isHovered() && isChecked()) return getCheckedHoverTexture(); - else if (isHovered()) return getDefaultHoverTexture(); - else if (isChecked()) return getCheckedTexture(); - else return getDefaultTexture(); - } - - public Identifier getCheckedTexture() { - return checkedTexture; - } - - public void setCheckedTexture(Identifier checkedTexture) { - this.checkedTexture = checkedTexture; - } - - public void setDefaultTexture(Identifier defaultTexture) { - this.defaultTexture = defaultTexture; - } - - public Identifier getDefaultTexture() { - return defaultTexture; - } - - public Identifier getCheckedHoverTexture() { - return checkedHoverTexture != null ? checkedHoverTexture : checkedTexture; - } - - public void setCheckedHoverTexture(@Nullable Identifier checkedHoverTexture) { - this.checkedHoverTexture = checkedHoverTexture; - } - - public Identifier getDefaultHoverTexture() { - return defaultHoverTexture != null ? defaultHoverTexture : defaultTexture; - } - - public void setDefaultHoverTexture(@Nullable Identifier defaultHoverTexture) { - this.defaultHoverTexture = defaultHoverTexture; - } - - public VColor getHoverOverlayColor() { - return hoverOverlayColor; - } - - public void setHoverOverlayColor(VColor hoverOverlayColor) { - this.hoverOverlayColor = hoverOverlayColor; - } - public VColor.ColorModifier modifyHoverOverlayColor() { - return new VColor.ColorModifier(hoverOverlayColor, this::setHoverOverlayColor); + if (event.equals(VEvents.Widget.LEFT_CLICK)) setChecked(!checked); } public boolean isChecked() { @@ -134,10 +41,10 @@ public boolean isChecked() { public void setChecked(boolean checked) { this.checked = checked; - fireEvent("vcheckbox-checked", checked); + events.fire(VEvents.CheckBox.CHECK_STATE_CHANGED, checked); } public void onCheckStateChange(VCheckedStateChange runnable) { - registerEventExecutor("vcheckbox-checked", args -> runnable.run((boolean) args[0])); + events.register(VEvents.CheckBox.CHECK_STATE_CHANGED, args -> runnable.run((boolean) args[0])); } } diff --git a/src/main/java/net/snackbag/vera/widget/VCompound.java b/src/main/java/net/snackbag/vera/widget/VCompound.java new file mode 100644 index 00000000..bdb80fa4 --- /dev/null +++ b/src/main/java/net/snackbag/vera/widget/VCompound.java @@ -0,0 +1,115 @@ +package net.snackbag.vera.widget; + +import net.snackbag.mcvera.MinecraftVera; +import net.snackbag.mcvera.impl.MCVeraRenderer; +import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VAppAccess; +import net.snackbag.vera.core.VRenderContext; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.layout.VLayout; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public abstract class VCompound> extends VWidget implements VAppAccess { + protected final VLayout layout; + private final List> widgets = new ArrayList<>(); + public final UUID identifier = UUID.randomUUID(); + + public VCompound(int x, int y, int width, int height, VLayout layout, VAppAccess app) { + super(x, y, width, height, app); + + this.layout = layout; + + move(x, y); + setSize(width, height); + + init(); + } + + public abstract void init(); + + // + // App Access + // + + @Override + public @NotNull VeraApp get() { + return getApp(); + } + + @Override + public List> getWidgets() { + return new ArrayList<>(widgets); + } + + @Override + public void addWidget(VWidget widget) { + if (widgets.contains(widget)) { + MinecraftVera.LOGGER.error("Can't add widget %s to compound %s, because it is already added" + .formatted(widget.toString(), getClass().getSimpleName())); + return; + } + + widget.classes.add(identifier.toString()); + widgets.add(widget); + layout.addElement(widget); + } + + @Override + public void removeWidget(VWidget widget) { + if (!widgets.contains(widget)) { + MinecraftVera.LOGGER.error("Can't remove widget %s from compound %s, because it wasn't added" + .formatted(widget.toString(), getClass().getSimpleName())); + return; + } + + widget.classes.add(identifier.toString()); + layout.removeElement(widget); + widgets.remove(widget); + } + + // + // Spoofing widget positions + // + + @Override + public void move(int x, int y) { + super.move(x, y); + layout.move(x, y); + } + + @Override + public void setSize(int width, int height) { + super.setSize(width, height); + layout.setSize(width, height); + } + + // + // Rendering + // + @Override + public void renderSelf() { + beforeRender(); + animations.updateLifetimes(); + + if (visibilityConditionsPassed()) { + VRenderContext ctx = createRenderContext(); + Vera.renderer.pushContext(ctx); + + renderContent(ctx); + + MCVeraRenderer.drawContext.getMatrices().translate(-getX(), -getY(), 0); + for (VWidget widget : widgets) widget.renderSelf(); + + renderBorder(ctx); + renderOverlay(ctx); + + Vera.renderer.popContext(); + } + + afterRender(); + } +} diff --git a/src/main/java/net/snackbag/vera/widget/VDropdown.java b/src/main/java/net/snackbag/vera/widget/VDropdown.java index 8ce4292f..c6270d94 100644 --- a/src/main/java/net/snackbag/vera/widget/VDropdown.java +++ b/src/main/java/net/snackbag/vera/widget/VDropdown.java @@ -3,42 +3,50 @@ import net.minecraft.util.Identifier; import net.snackbag.vera.Vera; import net.snackbag.vera.core.*; +import net.snackbag.vera.core.v4.V4Int; +import net.snackbag.vera.event.VEvents; import net.snackbag.vera.event.VItemSwitchEvent; -import net.snackbag.vera.modifier.VPaddingWidget; +import net.snackbag.vera.modifier.VHasFont; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.core.VRenderContext; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; -public class VDropdown extends VWidget implements VPaddingWidget { +// TODO: [Render Rework] +// TODO: Rewrite VDropdown from scratch +// 16/7/2025 jesus christ what a shitty thing. dont even bother making this work nice. +// rewrite scheduled for once we have VCompound +// 20/8/2025 oh my +public class VDropdown extends VWidget implements VHasFont { private final List items; - private VFont font; - private VFont hoverFont; - private VColor backgroundColor; + public VFont itemHoverFont; private VColor itemHoverColor; - private V4Int padding; private int selectedItem = 0; private int itemSpacing = 0; private @Nullable Integer hoveredItem = null; - public VDropdown(VeraApp app) { + public VDropdown(VAppAccess app) { super(0, 0, 100, 16, app); items = new ArrayList<>(); - font = VFont.create(); - hoverFont = VFont.create(); - backgroundColor = VColor.white(); + itemHoverFont = VFont.create(); itemHoverColor = VColor.white().sub(30); - padding = new V4Int(5, 10); - setHoverCursor(VCursorShape.POINTING_HAND); } @Override - public void render() { + public void renderContent(VRenderContext ctx) { + VStyleState state = createStyleState(); + + VColor backgroundColor = getStyle("background-color", state); + VFont font = getStyle("font", state); + V4Int padding = getStyle("padding", createStyleState()); + Vera.renderer.drawRect( - app, getHitboxX(), getHitboxY(), getHitboxWidth(), getHitboxHeight(), - 0, backgroundColor + ctx, 0, 0, getEffectiveWidth(), getEffectiveHeight(), + backgroundColor ); if (isFocused()) { @@ -46,33 +54,33 @@ app, getHitboxX(), getHitboxY(), getHitboxWidth(), getHitboxHeight(), boolean isHovered = hoveredItem != null && i == hoveredItem; Item item = items.get(i); - int textY = y + (i * (itemSpacing + font.getSize() / 2) + itemSpacing / 2); - int textX = (int) (item.icon == null ? x : x + font.getSize() * 0.7); + int textY = i * (itemSpacing + font.getSize() / 2) + itemSpacing / 2; + int textX = (int) (item.icon == null ? 0 : font.getSize() * 0.7); if (isHovered) { Vera.renderer.drawRect( - app, - getHitboxX(), - y + (i * (itemSpacing + font.getSize() / 2)), - getHitboxWidth(), + ctx, + 0, + i * (itemSpacing + font.getSize() / 2), + getEffectiveWidth(), font.getSize() / 2 + itemSpacing, - 0, itemHoverColor + itemHoverColor ); } if (item.icon != null) { Vera.renderer.drawImage( - app, x, textY, + ctx, 0, textY, font.getSize() / 2, font.getSize() / 2, - 0, isHovered ? item.getHoverIcon() : item.getIcon() + isHovered ? item.getHoverIcon() : item.getIcon() ); } - Vera.renderer.drawText(app, textX, textY, 0, item.name, isHovered ? hoverFont : font); + Vera.renderer.drawText(ctx, textX, textY, item.name, isHovered ? itemHoverFont : font); } } else { - Vera.renderer.drawText(app, x, y, 0, getItems().get(selectedItem).name, font); + Vera.renderer.drawText(ctx, padding.get3(), padding.get1(), getItems().get(selectedItem).name, font); } } @@ -80,8 +88,8 @@ app, getHitboxX(), getHitboxY(), getHitboxWidth(), getHitboxHeight(), public void setFocused(boolean focused) { super.setFocused(focused); - if (focused) fireEvent("vdropdown-selector-open"); - else fireEvent("vdropdown-selector-close"); + if (focused) events.fire(VEvents.Dropdown.SELECTOR_OPEN); + else events.fire(VEvents.Dropdown.SELECTOR_CLOSE); } public VColor getItemHoverColor() { @@ -92,62 +100,47 @@ public void setItemHoverColor(VColor itemHoveredColor) { this.itemHoverColor = itemHoveredColor; } - public VFont getHoverFont() { - return hoverFont; - } - - public void setHoverFont(VFont hoverFont) { - this.hoverFont = hoverFont; - } - - public VColor.ColorModifier modifyHoverFontColor() { - return new VColor.ColorModifier(hoverFont.getColor(), (color) -> setHoverFont(hoverFont.withColor(color))); - } - - public VFont.FontModifier modifyHoverFont() { - return new VFont.FontModifier(hoverFont, this::setHoverFont); - } - public VColor.ColorModifier modifyItemHoverColor() { return new VColor.ColorModifier(itemHoverColor, this::setItemHoverColor); } @Override - public int getHitboxX() { - return x - padding.get3(); + public int getEffectiveX() { + V4Int padding = getStyle("padding", createStyleState()); + return getX() - padding.get3(); } @Override - public int getHitboxY() { - return y - padding.get1(); + public int getEffectiveY() { + V4Int padding = getStyle("padding", createStyleState()); + return getY() - padding.get1(); } @Override - public int getHitboxWidth() { + public int getEffectiveWidth() { + V4Int padding = getStyle("padding", createStyleState()); return width + padding.get3() + padding.get4(); } @Override - public int getHitboxHeight() { + public int getEffectiveHeight() { + VStyleState state = createStyleState(); + + VFont font = getStyle("font", state); + V4Int padding = getStyle("padding", state); + return !isFocused() ? font.getSize() / 2 + padding.get1() + padding.get2() : items.size() * (font.getSize() / 2 + itemSpacing) + padding.get1() + padding.get2(); } - @Override - public V4Int getPadding() { - return padding; - } - - @Override - public void setPadding(V4Int padding) { - this.padding = padding; - } - @Override public void handleBuiltinEvent(String event, Object... args) { + int x = getX(); + int y = getY(); + switch (event) { - case "left-click" -> { + case VEvents.Widget.LEFT_CLICK -> { if (isFocused()) { Item target = getHoveredItem(); if (target != null && hoveredItem != null) { @@ -160,7 +153,7 @@ public void handleBuiltinEvent(String event, Object... args) { } } - case "right-click" -> { + case VEvents.Widget.RIGHT_CLICK -> { if (isFocused()) { Item target = getHoveredItem(); if (target != null && hoveredItem != null) { @@ -173,7 +166,7 @@ public void handleBuiltinEvent(String event, Object... args) { } } - case "middle-click" -> { + case VEvents.Widget.MIDDLE_CLICK -> { if (isFocused()) { Item target = getHoveredItem(); if (target != null && hoveredItem != null) { @@ -186,24 +179,22 @@ public void handleBuiltinEvent(String event, Object... args) { } } - case "mouse-move" -> { - if (!isFocused()) { - hoveredItem = null; - return; - } - - // Get mouse position relative to the dropdown's top-left corner - int argX = (int) args[0]; - int argY = (int) args[1]; + case VEvents.Widget.MOUSE_MOVE -> { + if (!isFocused()) hoveredItem = null; + else { + // Get mouse position relative to the dropdown's top-left corner + int argX = (int) args[0]; + int argY = (int) args[1]; - int mouseX = argX - x; - int mouseY = argY - y; + int mouseX = argX - x; + int mouseY = argY - y; - Item item = getItemAt(mouseX, mouseY); - hoveredItem = (item != null) ? items.indexOf(item) : null; + Item item = getItemAt(mouseX, mouseY); + hoveredItem = (item != null) ? items.indexOf(item) : null; + } } - case "hover-leave" -> hoveredItem = null; + case VEvents.Widget.HOVER_LEAVE -> hoveredItem = null; } super.handleBuiltinEvent(event, args); @@ -211,21 +202,22 @@ public void handleBuiltinEvent(String event, Object... args) { private int getItemIndexAt(int mouseY) { if (mouseY < 0) return -1; + VFont font = getStyle("font", createStyleState()); int index = mouseY / (itemSpacing + font.getSize() / 2); return items.size() < index ? -1 : index; } public void onItemSwitch(VItemSwitchEvent runnable) { - registerEventExecutor("vdropdown-item-switch", args -> runnable.run((int) args[0])); + events.register(VEvents.Dropdown.ITEM_SWITCH, args -> runnable.run((int) args[0])); } public void onSelectorOpen(Runnable runnable) { - registerEventExecutor("vdropdown-selector-open", runnable); + events.register(VEvents.Dropdown.SELECTOR_OPEN, runnable); } public void onSelectorClose(Runnable runnable) { - registerEventExecutor("vdropdown-selector-close", runnable); + events.register(VEvents.Dropdown.SELECTOR_CLOSE, runnable); } private @Nullable Item getItemAt(int mouseX, int mouseY) { @@ -258,35 +250,7 @@ public int getSelectedItem() { public void setSelectedItem(int selectedItem) { this.selectedItem = selectedItem; - fireEvent("vdropdown-item-switch", selectedItem); - } - - public VFont getFont() { - return font; - } - - public void setFont(VFont font) { - this.font = font; - } - - public VFont.FontModifier modifyFont() { - return new VFont.FontModifier(font, this::setFont); - } - - public VColor.ColorModifier modifyFontColor() { - return new VColor.ColorModifier(font.getColor(), (color) -> setFont(font.withColor(color))); - } - - public VColor getBackgroundColor() { - return backgroundColor; - } - - public void setBackgroundColor(VColor backgroundColor) { - this.backgroundColor = backgroundColor; - } - - public VColor.ColorModifier modifyBackgroundColor() { - return new VColor.ColorModifier(backgroundColor, this::setBackgroundColor); + events.fire(VEvents.Dropdown.ITEM_SWITCH, selectedItem); } public void addItem(String name) { diff --git a/src/main/java/net/snackbag/vera/widget/VImage.java b/src/main/java/net/snackbag/vera/widget/VImage.java deleted file mode 100644 index 59a2ce23..00000000 --- a/src/main/java/net/snackbag/vera/widget/VImage.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.snackbag.vera.widget; - -import net.minecraft.util.Identifier; -import net.snackbag.vera.Vera; -import net.snackbag.vera.core.VeraApp; - -public class VImage extends VWidget { - private Identifier path; - - public VImage(Identifier path, int width, int height, VeraApp app) { - super(0, 0, width, height, app); - - this.path = path; - this.focusOnClick = false; - } - - public Identifier getPath() { - return path; - } - - public void setPath(Identifier path) { - this.path = path; - } - - public void setPath(String path) { - setPath(new Identifier(path)); - } - - @Override - public void render() { - VeraApp app = getApp(); - Vera.renderer.drawImage(app, x, y, width, height, rotation, path); - } -} diff --git a/src/main/java/net/snackbag/vera/widget/VLabel.java b/src/main/java/net/snackbag/vera/widget/VLabel.java index e3680dea..00ca091e 100644 --- a/src/main/java/net/snackbag/vera/widget/VLabel.java +++ b/src/main/java/net/snackbag/vera/widget/VLabel.java @@ -2,48 +2,36 @@ import net.snackbag.vera.Vera; import net.snackbag.vera.core.*; -import net.snackbag.vera.modifier.VPaddingWidget; +import net.snackbag.vera.core.v4.V4Int; +import net.snackbag.vera.flag.VHAlignmentFlag; +import net.snackbag.vera.modifier.VHasFont; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.core.VRenderContext; -public class VLabel extends VWidget implements VPaddingWidget { +public class VLabel extends VWidget implements VHasFont { private String text; - private VFont font; - private VColor backgroundColor; - private V4Int padding; - private VAlignmentFlag alignment; + private VHAlignmentFlag alignment; - public VLabel(String text, VeraApp app) { - super(0, 0, 100, 16, app); + public VLabel(String text, int x, int y, int width, int height, VAppAccess app) { + super(x, y, width, height, app); this.text = text; - this.font = VFont.create(); - this.backgroundColor = VColor.transparent(); - this.padding = new V4Int(4); this.focusOnClick = false; - alignment = VAlignmentFlag.LEFT; + alignment = VHAlignmentFlag.LEFT; } - public String getText() { - return text; - } - - public VFont getFont() { - return font; - } - - public void setFont(VFont font) { - this.font = font; - } - - public VColor getBackgroundColor() { - return backgroundColor; + public VLabel(String text, int x, int y, VAppAccess app) { + this(text, x, y, 100, 16, app); + adjustSize(); } - public void setBackgroundColor(VColor backgroundColor) { - this.backgroundColor = backgroundColor; + public VLabel(String text, VAppAccess app) { + this(text, 0, 0, 100, 16, app); + adjustSize(); } - public VColor.ColorModifier modifyBackgroundColor() { - return new VColor.ColorModifier(backgroundColor, this::setBackgroundColor); + public String getText() { + return text; } public void setText(String text) { @@ -51,78 +39,62 @@ public void setText(String text) { } @Override - public V4Int getPadding() { - return padding; - } - - @Override - public void setPadding(V4Int padding) { - this.padding = padding; - } - - public VFont.FontModifier modifyFont() { - return new VFont.FontModifier(font, this::setFont); - } - - public VColor.ColorModifier modifyFontColor() { - return new VColor.ColorModifier(font.getColor(), (color) -> setFont(font.withColor(color))); - } - - @Override - public int getHitboxWidth() { + public int getEffectiveWidth() { + V4Int padding = getStyle("padding", createStyleState()); return width + padding.get3() + padding.get4(); } @Override - public int getHitboxHeight() { + public int getEffectiveHeight() { + V4Int padding = getStyle("padding", createStyleState()); return height + padding.get1() + padding.get2(); } - @Override - public int getHitboxX() { - return x - padding.get4(); - } - - @Override - public int getHitboxY() { - return y - padding.get1(); - } - - public VAlignmentFlag getAlignment() { + public VHAlignmentFlag getAlignment() { return alignment; } - public void setAlignment(VAlignmentFlag alignment) { + public void setAlignment(VHAlignmentFlag alignment) { this.alignment = alignment; } public void adjustSize() { + VFont font = getStyle("font", createStyleState()); + this.width = Vera.provider.getTextWidth(text, font); this.height = Vera.provider.getTextHeight(text, font); } + public VColor.ColorModifier modifyColor(String key) { + return getApp().styleSheet.modifyKeyAsColor(this, key); + } + @Override - public void render() { - VeraApp app = getApp(); - - Vera.renderer.drawRect( - app, - getHitboxX(), - getHitboxY(), - getHitboxWidth(), - getHitboxHeight(), - rotation, - backgroundColor - ); + public void renderContent(VRenderContext ctx) { + VStyleState state = createStyleState(); + + VFont font = getStyle("font", state); + VFill background = getStyle("background", state); + V4Int padding = getStyle("padding", state); + + if (background.isVisible()) { + Vera.renderer.drawFill( + ctx, + 0, + 0, + getEffectiveWidth(), + getEffectiveHeight(), + background + ); + } + + int usualX = padding.get3(); + int usualY = padding.get1(); switch (alignment) { - case LEFT -> Vera.renderer.drawText(app, x, y, rotation, text, font); - case CENTER -> { - int textWidth = Vera.provider.getTextWidth(text, font); - int centerX = getHitboxX() + (getHitboxWidth() - textWidth) / 2; - Vera.renderer.drawText(app, centerX, y, rotation, text, font); - } - case RIGHT -> Vera.renderer.drawText(app, getHitboxX() + getHitboxWidth() - padding.get4() - Vera.provider.getTextWidth(text, font), y, rotation, text, font); + case LEFT -> Vera.renderer.drawText(ctx, usualX, usualY, text, font); + case CENTER -> Vera.renderer.drawText(ctx, getEffectiveWidth() / 2 - Vera.provider.getTextWidth(text, font) / 2, usualY, text, font); + case RIGHT -> Vera.renderer.drawText(ctx, getEffectiveWidth() - padding.get4() - Vera.provider.getTextWidth(text, font), usualY, text, font); } } } diff --git a/src/main/java/net/snackbag/vera/widget/VLineInput.java b/src/main/java/net/snackbag/vera/widget/VLineInput.java index df2ff080..eae72f2c 100644 --- a/src/main/java/net/snackbag/vera/widget/VLineInput.java +++ b/src/main/java/net/snackbag/vera/widget/VLineInput.java @@ -2,149 +2,107 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.util.InputUtil; +import net.minecraft.util.math.MathHelper; import net.snackbag.vera.Vera; import net.snackbag.vera.core.*; +import net.snackbag.vera.core.v4.V4Int; +import net.snackbag.vera.event.VEvents; import net.snackbag.vera.event.VCharLimitedEvent; -import net.snackbag.vera.modifier.VPaddingWidget; +import net.snackbag.vera.modifier.VHasFont; +import net.snackbag.vera.modifier.VHasPlaceholderFont; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.core.VRenderContext; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; -public class VLineInput extends VWidget implements VPaddingWidget { +public class VLineInput extends VWidget implements VHasFont, VHasPlaceholderFont { private String text; private String placeholderText; - private VFont font; - private VFont placeholderFont; - private @Nullable VColor cursorColor; private int cursorPos; private TextSelection textSelection; - private VColor textSelectionColor; private int maxChars; + private long timeSinceLastInput; + private int textViewport = 0; - private VColor backgroundColor; - private V4Int padding; - - public VLineInput(VeraApp app) { + public VLineInput(VAppAccess app) { super(0, 0, 100, 20, app); this.text = ""; this.placeholderText = ""; - this.font = VFont.create(); - this.placeholderFont = VFont.create().withColor(VColor.black().withOpacity(0.5f)); - this.cursorColor = null; this.cursorPos = 0; this.textSelection = new TextSelection(); - this.textSelectionColor = VColor.of(0, 120, 215, 0.2f); this.maxChars = -1; - - this.backgroundColor = VColor.transparent(); - this.padding = new V4Int(4); - - setHoverCursor(VCursorShape.TEXT); + this.timeSinceLastInput = System.currentTimeMillis(); } @Override - public void render() { - Vera.renderer.drawRect( - app, - getHitboxX() + app.getX(), - getHitboxY() + app.getY(), - getHitboxWidth(), - getHitboxHeight(), - rotation, - backgroundColor - ); - - // Render text selection background + public void renderContent(VRenderContext ctx) { + VStyleState state = createStyleState(); + + VFont font = getStyle("font", state); + VFont placeholderFont = getStyle("placeholder-font", state); + VFill background = getStyle("background", state); + VFill textSelectionFill = getStyle("select", state); + V4Int padding = getStyle("padding", state); + String rText = getTextInViewport(); + + // background + Vera.renderer.drawFill(ctx, 0, 0, getEffectiveWidth(), getEffectiveHeight(), background); + + // text selection + int textHeight = Vera.provider.getTextHeight(text, font); + int textWidth = Vera.provider.getTextWidth(rText, font); + int textX = Vera.provider.getTextWidth(text, font) < width + ? padding.get3() + : padding.get3() - textWidth + width; + int textY = padding.get1() + height / 2 - textHeight / 2; + if (!textSelection.isClear()) { int selStart = Math.min(textSelection.startPos, textSelection.endPos); - int selEnd = Math.max(textSelection.startPos, textSelection.endPos); - String beforeSelection = text.substring(0, selStart); - String selectedText = text.substring(selStart, selEnd); - - int selectionX = x + Vera.provider.getTextWidth(beforeSelection, font); - Vera.renderer.drawRect( - app, - selectionX, - y, - Vera.provider.getTextWidth(selectedText, font), - Vera.provider.getTextHeight(text, font), - 0, - textSelectionColor - ); + int selEnd = Math.max(textSelection.startPos, textSelection.endPos); + + // clamp selection to the visible viewport + int visStart = Math.max(selStart, textViewport); + int visEnd = Math.min(selEnd, getTextViewportEnd()); + + if (visStart < visEnd) { + String beforeSel = rText.substring(0, visStart - textViewport); + String selInView = rText.substring(visStart - textViewport, visEnd - textViewport); + + int startX = textX + Vera.provider.getTextWidth(beforeSel, font); + int selTextWidth = Vera.provider.getTextWidth(selInView, font); + int selTextHeight = Vera.provider.getTextHeight(selInView, font); + + Vera.renderer.drawFill(ctx, startX, textY, selTextWidth, selTextHeight, textSelectionFill); + } } - if (text.isEmpty()) Vera.renderer.drawText(app, x, y, 0, placeholderText, placeholderFont); - else Vera.renderer.drawText(app, x, y, 0, text, font); + // text + if (text.isEmpty()) Vera.renderer.drawText(ctx, textX, textY, placeholderText, placeholderFont); + else Vera.renderer.drawText(ctx, textX, textY, rText, font); - if (isFocused() && textSelection.isClear() && (System.currentTimeMillis() / 500) % 2 == 0) { - Vera.renderer.drawRect( - app, - x + Vera.provider.getTextWidth(text.substring(0, cursorPos), font), - y, - 1, - Vera.provider.getTextHeight(text, font), - 0, - getCursorColorSafe() + // cursor + if (isFocused() && textSelection.isClear() && ((System.currentTimeMillis() - timeSinceLastInput) / 500) % 2 == 0) { + int cursorX = textX + Vera.provider.getTextWidth( + rText.substring(0, Math.min( + cursorPos - textViewport, + rText.length() + )), font ); + Vera.renderer.drawRect(ctx, cursorX, textY, 1, textHeight, getCursorColorSafe()); } } @Override public void handleBuiltinEvent(String event, Object... args) { - if (event.equals("left-click")) { - textSelection.clear(); - - if (Vera.getMouseX() < x) cursorPos = 0; - else if (Vera.getMouseX() > x + Vera.provider.getTextWidth(text, font)) cursorPos = text.length(); - } - super.handleBuiltinEvent(event, args); - } - - public VFont getFont() { - return font; - } - - public void setFont(VFont font) { - this.font = font; - } - - public VFont getPlaceholderFont() { - return placeholderFont; - } - - public void setPlaceholderFont(VFont placeholderFont) { - this.placeholderFont = placeholderFont; - } - - public VFont.FontModifier modifyFont() { - return new VFont.FontModifier(font, this::setFont); - } - - public VColor.ColorModifier modifyFontColor() { - return new VColor.ColorModifier(font.getColor(), (color) -> setFont(font.withColor(color))); - } - - public VFont.FontModifier modifyPlaceholderFont() { - return new VFont.FontModifier(font, this::setPlaceholderFont); - } - - public VColor.ColorModifier modifyPlaceholderFontColor() { - return new VColor.ColorModifier(font.getColor(), (color) -> setFont(font.withColor(color))); - } - - public VColor getBackgroundColor() { - return backgroundColor; - } - - public void setBackgroundColor(VColor backgroundColor) { - this.backgroundColor = backgroundColor; - } - public VColor.ColorModifier modifyBackgroundColor() { - return new VColor.ColorModifier(backgroundColor, this::setBackgroundColor); + if (event.equals(VEvents.Widget.LEFT_CLICK)) { + textSelection.clear(); + setCursorPos(getCharPosAtX(getRelativeMouseX())); + } } public String getText() { @@ -153,7 +111,16 @@ public String getText() { public void setText(String text) { this.text = text; - fireEvent("vline-change"); + this.timeSinceLastInput = System.currentTimeMillis(); + events.fire(VEvents.LineInput.CHANGE); + } + + private String getTextInViewport() { + return text.substring(textViewport, getTextViewportEnd()); + } + + public long getTimeSinceLastInput() { + return timeSinceLastInput; } public boolean isSelectingText() { @@ -177,18 +144,6 @@ public void setTextSelection(int start, int end) { textSelection.setEndPos(end); } - public VColor getTextSelectionColor() { - return textSelectionColor; - } - - public void setTextSelectionColor(VColor textSelectionColor) { - this.textSelectionColor = textSelectionColor; - } - - public VColor.ColorModifier modifyTextSelectionColor() { - return new VColor.ColorModifier(textSelectionColor, this::setTextSelectionColor); - } - public int getMaxChars() { return maxChars; } @@ -210,23 +165,23 @@ public String getPlaceholderText() { } public void onLineChanged(Runnable runnable) { - registerEventExecutor("vline-change", runnable); + events.register(VEvents.LineInput.CHANGE, runnable); } public void onCursorMove(Runnable runnable) { - registerEventExecutor("vline-cursor-move", runnable); + events.register(VEvents.LineInput.CURSOR_MOVE, runnable); } public void onCursorMoveLeft(Runnable runnable) { - registerEventExecutor("vline-cursor-move-left", runnable); + events.register(VEvents.LineInput.CURSOR_MOVE_LEFT, runnable); } public void onCursorMoveRight(Runnable runnable) { - registerEventExecutor("vline-cursor-move-right", runnable); + events.register(VEvents.LineInput.CURSOR_MOVE_RIGHT, runnable); } public void onAddCharLimited(VCharLimitedEvent runnable) { - registerEventExecutor("vline-add-char-limited", args -> runnable.run((char) args[0])); + events.register(VEvents.LineInput.ADD_CHAR_LIMITED, args -> runnable.run((char) args[0])); } @Override @@ -310,33 +265,27 @@ else if (keyCode == GLFW.GLFW_KEY_BACKSPACE && cursorPos > 0) { } // Handle word navigation else if (isDown(GLFW.GLFW_KEY_LEFT) && isAltDown() && cursorPos > 0) { - cursorPos = Math.max(0, jumpToWordStart(cursorPos)); - fireEvent("vline-cursor-move"); - fireEvent("vline-cursor-move-left"); + setCursorPos(Math.max(0, jumpToWordStart(cursorPos))); + events.fire(VEvents.LineInput.CURSOR_MOVE_LEFT); } else if (isDown(GLFW.GLFW_KEY_RIGHT) && isAltDown() && cursorPos < text.length()) { - cursorPos = Math.min(text.length(), jumpToWordEnd(cursorPos)); - fireEvent("vline-cursor-move"); - fireEvent("vline-cursor-move-right"); + setCursorPos(Math.min(text.length(), jumpToWordEnd(cursorPos))); + events.fire(VEvents.LineInput.CURSOR_MOVE_LEFT); } // Handle line navigation else if (isDown(GLFW.GLFW_KEY_LEFT) && isCtrlDown()) { - cursorPos = 0; - fireEvent("vline-cursor-move"); - fireEvent("vline-cursor-move-left"); + setCursorPos(0); + events.fire(VEvents.LineInput.CURSOR_MOVE_LEFT); } else if (isDown(GLFW.GLFW_KEY_RIGHT) && isCtrlDown()) { - cursorPos = text.length(); - fireEvent("vline-cursor-move"); - fireEvent("vline-cursor-move-right"); + setCursorPos(text.length()); + events.fire(VEvents.LineInput.CURSOR_MOVE_RIGHT); } // Handle character navigation else if (keyCode == GLFW.GLFW_KEY_LEFT && cursorPos > 0) { - cursorPos = Math.max(0, cursorPos - 1); - fireEvent("vline-cursor-move"); - fireEvent("vline-cursor-move-left"); + setCursorPos(Math.max(0, cursorPos - 1)); + events.fire(VEvents.LineInput.CURSOR_MOVE_LEFT); } else if (keyCode == GLFW.GLFW_KEY_RIGHT && cursorPos < text.length()) { - cursorPos = Math.min(text.length(), cursorPos + 1); - fireEvent("vline-cursor-move"); - fireEvent("vline-cursor-move-right"); + setCursorPos(Math.min(text.length(), cursorPos + 1)); + events.fire(VEvents.LineInput.CURSOR_MOVE_RIGHT); } super.keyPressed(keyCode, scanCode, modifiers); @@ -366,22 +315,20 @@ private void handleSelectionKeyPress(int keyCode) { } } - cursorPos = newPos; + setCursorPos(newPos); textSelection.endPos = newPos; - fireEvent("vline-cursor-move"); } private void insertText(String insertion) { if (maxChars > -1 && text.length() + insertion.length() > maxChars) { - fireEvent("vline-add-char-limited", insertion.charAt(0)); + events.fire(VEvents.LineInput.ADD_CHAR_LIMITED, insertion.charAt(0)); return; } String front = text.substring(0, cursorPos); String back = text.substring(cursorPos); - text = front + insertion + back; - cursorPos += insertion.length(); - fireEvent("vline-change"); + setText(front + insertion + back); + setCursorPos(cursorPos + insertion.length()); } private void deleteSelectedText() { @@ -392,10 +339,10 @@ private void deleteSelectedText() { String front = text.substring(0, start); String back = text.substring(end); - text = front + back; - cursorPos = start; + setText(front + back); + setCursorPos(start); +// setTextViewport(textViewport - (end - start)); clearTextSelection(); - fireEvent("vline-change"); } private void replaceSelectedText(String replacement) { @@ -405,16 +352,16 @@ private void replaceSelectedText(String replacement) { int end = Math.max(textSelection.startPos, textSelection.endPos); if (maxChars > -1 && text.length() - (end - start) + replacement.length() > maxChars) { - fireEvent("vline-add-char-limited", replacement.charAt(0)); + events.fire(VEvents.LineInput.ADD_CHAR_LIMITED, replacement.charAt(0)); return; } String front = text.substring(0, start); String back = text.substring(end); - text = front + replacement + back; - cursorPos = start + replacement.length(); + setText(front + replacement + back); + setCursorPos(start + replacement.length()); +// setTextViewport(textViewport - (end - start)); clearTextSelection(); - fireEvent("vline-change"); } @@ -425,55 +372,114 @@ private String getSelectedText() { return text.substring(start, end); } - public int getCursorPos() { - return cursorPos; + public void setTextViewport(int textViewport) { + this.textViewport = MathHelper.clamp(textViewport, 0, text.length()); } - public void setCursorPos(int cursorPos) { - this.cursorPos = cursorPos; + public int getTextViewportBegin() { + return textViewport; } - public @Nullable VColor getCursorColor() { - return cursorColor; - } + private int getTextViewportEnd() { + VStyleState state = createStyleState(); + VFont font = getStyle("font", state); - public VColor getCursorColorSafe() { - return cursorColor == null ? font.getColor() : cursorColor; - } + StringBuilder buf = new StringBuilder(); + for (int i = textViewport; i < text.length(); i++) { + buf.append(text.charAt(i)); + if (Vera.provider.getTextWidth(buf.toString(), font) <= width) continue; + return i; + } - public void setCursorColor(@Nullable VColor cursorColor) { - this.cursorColor = cursorColor; + return text.length(); } - @Override - public V4Int getPadding() { - return padding; - } + private void updateTextViewport() { + textViewport = MathHelper.clamp(textViewport, 0, text.length()); - @Override - public void setPadding(V4Int padding) { - this.padding = padding; + // Cursor is before the viewport: snap left + if (cursorPos < textViewport) { + textViewport = cursorPos; + } + + // cursor is past the viewport end: advance right + int end; + while ((end = getTextViewportEnd()) <= cursorPos && end < text.length()) { + textViewport++; + } + + // if the end of the text is visible, try scrolling back left + while (textViewport > 0 && getTextViewportEnd() == text.length()) { + textViewport--; + } + // if that last decrement caused overflow that hides the cursor, undo it + if (getTextViewportEnd() < text.length() && cursorPos >= getTextViewportEnd()) { + textViewport++; + } } - @Override - public int getHitboxWidth() { - return Math.max(width, Vera.provider.getTextWidth(text, font)) + padding.get3() + padding.get4(); + public int getCursorPos() { + return cursorPos; } - @Override - public int getHitboxHeight() { - return Vera.provider.getTextHeight(text, font) + padding.get1() + padding.get2(); + public void setCursorPos(int cursorPos) { + this.cursorPos = cursorPos; + this.timeSinceLastInput = System.currentTimeMillis(); + events.fire(VEvents.LineInput.CURSOR_MOVE); + updateTextViewport(); + } + + public int getCharPosAtX(int x) { + VStyleState state = createStyleState(); + VFont font = getStyle("font", state); + V4Int padding = getStyle("padding", state); + + String rText = getTextInViewport(); + int rTextWidth = Vera.provider.getTextWidth(rText, font); + int textX = Vera.provider.getTextWidth(text, font) < width + ? padding.get3() + : padding.get3() - rTextWidth + width; + + if (x <= textX) { + return textViewport; + } else { + int bestPos = rText.length(); + for (int i = 0; i < rText.length(); i++) { + int charMidX = textX + + Vera.provider.getTextWidth(rText.substring(0, i), font) + + Vera.provider.getTextWidth(rText.substring(i, i + 1), font) / 2; + if (x <= charMidX) { + bestPos = i; + break; + } + } + return textViewport + bestPos; + } } + public VColor getCursorColorSafe() { + VStyleState state = createStyleState(); + + VColor style = getStyleOrDefault("cursor-color", null, state); + VFont font = getStyle("font", state); + + return style == null ? font.getColor() : style; + } @Override - public int getHitboxX() { - return x - padding.get4(); + public int getEffectiveWidth() { + VStyleState state = createStyleState(); + V4Int padding = getStyle("padding", state); + + return width + padding.get3() + padding.get4(); } @Override - public int getHitboxY() { - return y - padding.get1(); + public int getEffectiveHeight() { + VStyleState state = createStyleState(); + V4Int padding = getStyle("padding", state); + + return height + padding.get1() + padding.get2(); } @Override @@ -485,30 +491,28 @@ public void charTyped(char chr, int modifiers) { int end = Math.max(textSelection.startPos, textSelection.endPos); if (maxChars > -1 && text.length() - (end - start) + 1 > maxChars) { - fireEvent("vline-add-char-limited", chr); + events.fire(VEvents.LineInput.ADD_CHAR_LIMITED, chr); return; } String front = text.substring(0, start); String back = text.substring(end); - text = front + chr + back; - cursorPos = start + 1; + setText(front + chr + back); + setCursorPos(start + 1); clearTextSelection(); - fireEvent("vline-change"); } else { // Normal character insertion if (maxChars > -1 && text.length() >= maxChars) { - fireEvent("vline-add-char-limited", chr); + events.fire(VEvents.LineInput.ADD_CHAR_LIMITED, chr); return; } String front = text.substring(0, cursorPos); String back = text.substring(cursorPos); - text = front + chr + back; - cursorPos += 1; - fireEvent("vline-change"); + setText(front + chr + back); + setCursorPos(cursorPos + 1); } } super.charTyped(chr, modifiers); @@ -575,7 +579,7 @@ private int jumpToWordEnd(int position) { public void selectAll() { textSelection.startPos = 0; textSelection.endPos = text.length(); - cursorPos = text.length(); + setCursorPos(text.length()); } private void deleteText(int start, int end) { @@ -585,9 +589,9 @@ private void deleteText(int start, int end) { StringBuilder builder = new StringBuilder(text); builder.delete(start, end); - text = builder.toString(); - cursorPos = Math.min(start, text.length()); - fireEvent("vline-change"); + setText(builder.toString()); + setCursorPos(Math.min(start, text.length())); +// setTextViewport(textViewport - (end - start)); } public static class TextSelection { diff --git a/src/main/java/net/snackbag/vera/widget/VRect.java b/src/main/java/net/snackbag/vera/widget/VRect.java index 59fbf060..2ef4cbbc 100644 --- a/src/main/java/net/snackbag/vera/widget/VRect.java +++ b/src/main/java/net/snackbag/vera/widget/VRect.java @@ -1,33 +1,29 @@ package net.snackbag.vera.widget; import net.snackbag.vera.Vera; -import net.snackbag.vera.core.VColor; -import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.core.*; +import net.snackbag.vera.style.VStyleState; public class VRect extends VWidget { - protected VColor color; - - public VRect(VColor color, VeraApp app) { - super(0, 0, 20, 20, app); - - this.color = color; - this.focusOnClick = false; + public VRect(VFill background, VAppAccess app) { + this(background, 0, 0, 20, 20, app); } - public VColor getColor() { - return color; + public VRect(VFill background, int x, int y, VAppAccess app) { + this(background, x, y, 20, 20, app); } - public void setColor(VColor color) { - this.color = color; - } + public VRect(VFill background, int x, int y, int width, int height, VAppAccess app) { + super(x, y, width, height, app); - public VColor.ColorModifier modifyColor() { - return new VColor.ColorModifier(color, this::setColor); + this.focusOnClick = false; + setStyle("background", background); } @Override - public void render() { - Vera.renderer.drawRect(app, x, y, width, height, rotation, color); + public void renderContent(VRenderContext ctx) { + VStyleState state = createStyleState(); + + Vera.renderer.drawFill(ctx, 0, 0, width, height, getStyle("background", state)); } } diff --git a/src/main/java/net/snackbag/vera/widget/VTabWidget.java b/src/main/java/net/snackbag/vera/widget/VTabWidget.java index 9f68fb0b..456cc0f4 100644 --- a/src/main/java/net/snackbag/vera/widget/VTabWidget.java +++ b/src/main/java/net/snackbag/vera/widget/VTabWidget.java @@ -1,39 +1,34 @@ package net.snackbag.vera.widget; -import net.minecraft.client.MinecraftClient; import net.snackbag.vera.Vera; -import net.snackbag.vera.core.VColor; -import net.snackbag.vera.core.VCursorShape; -import net.snackbag.vera.core.VFont; -import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.core.*; +import net.snackbag.vera.event.VEvents; +import net.snackbag.vera.modifier.VHasFont; +import net.snackbag.vera.style.VStyleState; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.function.Consumer; -public class VTabWidget extends VWidget { - private VFont font; - private VColor selectedBackgroundColor; - private VColor defaultBackgroundColor; - private int itemSpacingLeft = 4; - private int itemSpacingRight = 4; - +public class VTabWidget extends VWidget implements VHasFont { private final LinkedHashMap>> tabs = new LinkedHashMap<>(); private @Nullable Integer activeTab = null; private @Nullable Integer hoveredTab = null; - public VTabWidget(VeraApp app, String... tabs) { + public VTabWidget(VAppAccess app) { super(0, 0, 100, 16, app); - - font = VFont.create(); - selectedBackgroundColor = VColor.white(); - defaultBackgroundColor = VColor.white().sub(40); - - setHoverCursor(VCursorShape.POINTING_HAND); } @Override - public void render() { + public void renderContent(VRenderContext ctx) { + VStyleState state = createStyleState(); + + VFont font = getStyle("font", state); + VColor defaultBackgroundColor = getStyle("background-color", state); + VColor selectedBackgroundColor = getStyle("background-color-selected", state); + int itemSpacingLeft = getStyle("item-spacing-left", state); + int itemSpacingRight = getStyle("item-spacing-right", state); + int marginX = 0; int i = -1; @@ -43,14 +38,14 @@ public void render() { i++; marginX += itemSpacingLeft; - Vera.renderer.drawRect(app, - x + marginX - itemSpacingLeft, y, + Vera.renderer.drawRect(ctx, + marginX - itemSpacingLeft, 0, itemSpacingLeft + itemSpacingRight + textWidth, - getHitboxHeight(), 0, + getEffectiveHeight(), activeTab != null && activeTab == i ? selectedBackgroundColor: defaultBackgroundColor ); - Vera.renderer.drawText(app, x + marginX, y + 2, 0, key, font); + Vera.renderer.drawText(ctx, marginX, 2, key, font); marginX += textWidth + itemSpacingRight; } @@ -58,42 +53,42 @@ public void render() { @Override public void handleBuiltinEvent(String event, Object... args) { + super.handleBuiltinEvent(event, args); + switch (event) { - case "mouse-move" -> getHoveredTabIndex((int) args[0]); + case VEvents.Widget.MOUSE_MOVE -> getHoveredTabIndex((int) args[0]); - case "hover-enter" -> getHoveredTabIndex(Vera.getMouseX()); - case "hover-leave" -> hoveredTab = null; + case VEvents.Widget.HOVER -> getHoveredTabIndex(Vera.getMouseX()); + case VEvents.Widget.HOVER_LEAVE -> hoveredTab = null; - case "left-click" -> { + case VEvents.Widget.LEFT_CLICK -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-left-click", hoveredTab); + events.fire(VEvents.TabWidget.TAB_LEFT_CLICK, hoveredTab); setActiveTab(hoveredTab); } - case "left-click-release" -> { + case VEvents.Widget.LEFT_CLICK_RELEASE -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-left-click-release", hoveredTab); + events.fire(VEvents.TabWidget.TAB_LEFT_CLICK_RELEASE, hoveredTab); } - case "middle-click" -> { + case VEvents.Widget.MIDDLE_CLICK -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-middle-click", hoveredTab); + events.fire(VEvents.TabWidget.TAB_MIDDLE_CLICK, hoveredTab); } - case "middle-click-release" -> { + case VEvents.Widget.MIDDLE_CLICK_RELEASE -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-middle-click-release", hoveredTab); + events.fire(VEvents.TabWidget.TAB_MIDDLE_CLICK_RELEASE, hoveredTab); } - case "right-click" -> { + case VEvents.Widget.RIGHT_CLICK -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-right-click", hoveredTab); + events.fire(VEvents.TabWidget.TAB_RIGHT_CLICK, hoveredTab); } - case "right-click-release" -> { + case VEvents.Widget.RIGHT_CLICK_RELEASE -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-right-click-release", hoveredTab); + events.fire(VEvents.TabWidget.TAB_RIGHT_CLICK_RELEASE, hoveredTab); } } - - super.handleBuiltinEvent(event, args); } public boolean isValidTabIndex(@Nullable Integer index) { @@ -106,7 +101,13 @@ public boolean isValidTabIndex(@Nullable Integer index) { } public int getHoveredTabIndex(int mouseX) { - int relativeX = mouseX - getHitboxX(); + VStyleState state = createStyleState(); + + VFont font = getStyle("font", state); + int itemSpacingLeft = getStyle("item-spacing-left", state); + int itemSpacingRight = getStyle("item-spacing-right", state); + + int relativeX = mouseX - getX(); int currentX = 0; int index = 0; @@ -116,7 +117,7 @@ public int getHoveredTabIndex(int mouseX) { if (relativeX >= currentX && relativeX < currentX + totalTabWidth) { if (hoveredTab != null && hoveredTab != index) { - fireEvent("vtabwidget-tab-hover-change", hoveredTab); + events.fire(VEvents.TabWidget.TAB_HOVER_CHANGE, hoveredTab); } hoveredTab = index; @@ -135,31 +136,31 @@ public int getHoveredTabIndex(int mouseX) { } public void onTabHoverChange(Consumer runnable) { - registerEventExecutor("vtabwidget-tab-hover-change", (args) -> runnable.accept((int) args[0])); + events.register(VEvents.TabWidget.TAB_HOVER_CHANGE, (args) -> runnable.accept((int) args[0])); } public void onTabLeftClick(Consumer runnable) { - registerEventExecutor("vtabwidget-tab-left-click", (args) -> runnable.accept((int) args[0])); + events.register(VEvents.TabWidget.TAB_LEFT_CLICK, (args) -> runnable.accept((int) args[0])); } public void onTabLeftClickRelease(Consumer runnable) { - registerEventExecutor("vtabwidget-tab-left-click-release", (args) -> runnable.accept((int) args[0])); + events.register(VEvents.TabWidget.TAB_LEFT_CLICK_RELEASE, (args) -> runnable.accept((int) args[0])); } public void onTabMiddleClick(Consumer runnable) { - registerEventExecutor("vtabwidget-tab-middle-click", (args) -> runnable.accept((int) args[0])); + events.register(VEvents.TabWidget.TAB_MIDDLE_CLICK, (args) -> runnable.accept((int) args[0])); } public void onTabMiddleClickRelease(Consumer runnable) { - registerEventExecutor("vtabwidget-tab-middle-click-release", (args) -> runnable.accept((int) args[0])); + events.register(VEvents.TabWidget.TAB_MIDDLE_CLICK_RELEASE, (args) -> runnable.accept((int) args[0])); } public void onTabRightClick(Consumer runnable) { - registerEventExecutor("vtabwidget-tab-right-click", (args) -> runnable.accept((int) args[0])); + events.register(VEvents.TabWidget.TAB_RIGHT_CLICK, (args) -> runnable.accept((int) args[0])); } public void onTabRightClickRelease(Consumer runnable) { - registerEventExecutor("vtabwidget-tab-right-click-release", (args) -> runnable.accept((int) args[0])); + events.register(VEvents.TabWidget.TAB_RIGHT_CLICK_RELEASE, (args) -> runnable.accept((int) args[0])); } public void addTab(String tab, VWidget... widgets) { @@ -180,7 +181,7 @@ public void addWidget(String tab, VWidget... widgets) { public void addWidget(String tab, List> widgets) { if (tab == null || !tabs.containsKey(tab)) { - throw new IllegalArgumentException("Failed to add " + widgets.size() + " widget(s) to tab '" + tab + "', because it doesn't exist. (App: " + app.getClass().getSimpleName() + ")"); + throw new IllegalArgumentException("Failed to add " + widgets.size() + " widget(s) to tab '" + tab + "', because it doesn't exist. (App: " + getApp().getClass().getSimpleName() + ")"); } Integer tabIndex = getTabIndex(tab); @@ -192,12 +193,18 @@ public void addWidget(String tab, List> widgets) { } @Override - public int getHitboxHeight() { - return font.getSize() / 2 + 4; + public int getEffectiveHeight() { + return ((VFont) getStyle("font", createStyleState())).getSize() / 2 + 4; } @Override - public int getHitboxWidth() { + public int getEffectiveWidth() { + VStyleState state = createStyleState(); + + VFont font = getStyle("font", state); + int itemSpacingLeft = getStyle("item-spacing-left", state); + int itemSpacingRight = getStyle("item-spacing-right", state); + int currentX = 0; for (String tabName : tabs.keySet()) { @@ -222,59 +229,11 @@ public void setActiveTab(@Nullable Integer activeTab) { this.activeTab = activeTab; } - public VFont getFont() { - return font; - } - - public void setFont(VFont font) { - this.font = font; - } - - public VFont.FontModifier modifyFont() { - return new VFont.FontModifier(font, this::setFont); - } - - public VColor.ColorModifier modifyFontColor() { - return new VColor.ColorModifier(font.getColor(), (color) -> setFont(getFont().withColor(color))); - } - - public VColor getSelectedBackgroundColor() { - return selectedBackgroundColor; - } - - public void setSelectedBackgroundColor(VColor backgroundColor) { - this.selectedBackgroundColor = backgroundColor; - } - - public VColor.ColorModifier modifySelectedBackgroundColor() { - return new VColor.ColorModifier(selectedBackgroundColor, this::setSelectedBackgroundColor); - } - - public VColor getDefaultBackgroundColor() { - return defaultBackgroundColor; - } - - public void setDefaultBackgroundColor(VColor defaultBackgroundColor) { - this.defaultBackgroundColor = defaultBackgroundColor; - } - - public VColor.ColorModifier modifyDefaultBackgroundColor() { - return new VColor.ColorModifier(defaultBackgroundColor, this::setDefaultBackgroundColor); - } - - public int getItemSpacingLeft() { - return itemSpacingLeft; - } - - public void setItemSpacingLeft(int itemSpacingLeft) { - this.itemSpacingLeft = itemSpacingLeft; - } - - public int getItemSpacingRight() { - return itemSpacingRight; + public VColor.ColorModifier modifyBackgroundColorSelected() { + return getApp().styleSheet.modifyKeyAsColor(this, "background-color-selected"); } - public void setItemSpacingRight(int itemSpacingRight) { - this.itemSpacingRight = itemSpacingRight; + public VColor.ColorModifier modifyBackgroundColor() { + return getApp().styleSheet.modifyKeyAsColor(this, "background-color"); } } diff --git a/src/main/java/net/snackbag/vera/widget/VWidget.java b/src/main/java/net/snackbag/vera/widget/VWidget.java index d31bb957..3df12fc5 100644 --- a/src/main/java/net/snackbag/vera/widget/VWidget.java +++ b/src/main/java/net/snackbag/vera/widget/VWidget.java @@ -1,69 +1,46 @@ package net.snackbag.vera.widget; +import net.snackbag.vera.VElement; import net.snackbag.vera.Vera; import net.snackbag.vera.core.*; +import net.snackbag.vera.core.v4.V4Color; +import net.snackbag.vera.core.v4.V4Int; import net.snackbag.vera.event.*; -import org.jetbrains.annotations.Nullable; +import net.snackbag.vera.layout.VLayout; +import net.snackbag.vera.style.VStyleState; +import net.snackbag.vera.style.animation.AnimationEngine; +import net.snackbag.vera.style.animation.CompiledAnimation; +import net.snackbag.vera.style.animation.VAnimation; +import net.snackbag.vera.style.animation.easing.VEasing; +import net.snackbag.vera.util.DragHandler; +import net.snackbag.vera.core.VRenderContext; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.function.Supplier; - -public abstract class VWidget> { - protected int x; - protected int y; - protected int width; - protected int height; - protected double rotation; - protected V4Color border; - protected V4Int borderSize; - - protected VeraApp app; - protected VCursorShape hoverCursor = VCursorShape.DEFAULT; - protected @Nullable VCursorShape cursorBeforeHover = null; - protected boolean focusOnClick = true; +import java.util.*; + +public abstract class VWidget> extends VElement { + public AnimationEngine animations = new AnimationEngine(this); + protected boolean hasTransparency = false; + + public boolean focusOnClick = true; private boolean hovered = false; - private boolean visible = true; private boolean leftClickDown = false; private boolean middleClickDown = false; private boolean rightClickDown = false; - private int leftDragPreviousX = -1; - private int leftDragPreviousY = -1; - private int middleDragPreviousX = -1; - private int middleDragPreviousY = -1; - private int rightDragPreviousX = -1; - private int rightDragPreviousY = -1; + private VStyleState handledPrevStyleState = VStyleState.DEFAULT; // constantly updates + private VStyleState prevStyleState = VStyleState.DEFAULT; // updates max once per frame, can be seen as the definite result - private final HashMap> eventExecutors; - private final List> visibilityConditions; + private VStyleState transitionOrigin = null; + private boolean isTransitionUnwinding = false; - public VWidget(int x, int y, int width, int height, VeraApp app) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.app = app; - this.rotation = 0; - this.eventExecutors = new HashMap<>(); - this.visibilityConditions = new ArrayList<>(); - this.border = new V4Color(VColor.black()); - this.borderSize = new V4Int(0); + public final LinkedHashSet classes = new LinkedHashSet<>(); - addVisibilityCondition(this::isVisible); + public VWidget(int x, int y, int width, int height, VAppAccess app) { + super(app, x, y, width, height); } - public abstract void render(); - - public int getX() { - return x; - } - - public int getY() { - return y; - } + public abstract void renderContent(VRenderContext ctx); public int getHitboxX() { return getX(); @@ -73,112 +50,221 @@ public int getHitboxY() { return getY(); } - public int getWidth() { - return width; + public int getHitboxWidth() { + return getEffectiveWidth(); } - public int getHitboxWidth() { - return getWidth(); + public int getHitboxHeight() { + return getEffectiveHeight(); } - public int getHeight() { - return height; + @SuppressWarnings("unchecked") + public void setStyle(String key, V... value) { + getApp().styleSheet.setKey(this, key, value); } - public int getHitboxHeight() { - return getHeight(); + @SuppressWarnings("unchecked") + public void setStyle(String key, VStyleState state, V... value) { + getApp().styleSheet.setKey(this, key, value, state); + } + + public V getStyle(String key) { + return animations.animateStyle(key, getApp().styleSheet.getKey(this, key)); + } + + public V getStyle(String key, VStyleState state) { + return animations.animateStyle(key, getApp().styleSheet.getKey(this, key, state)); + } + + public V getStyleOrDefault(String key, V dflt) { + V style = getStyle(key); + return style != null ? style : dflt; } - public void setWidth(int width) { - this.width = width; + public V getStyleOrDefault(String key, V dflt, VStyleState state) { + V style = getStyle(key, state); + return style != null ? style : dflt; } - public void setHeight(int height) { - this.height = height; + public void animate(VAnimation animation) { + animate(animation, false); } - public V4Color getBorder() { - return border; + public void animate(VAnimation animation, boolean override) { + if (override && isAnimationActive(animation.name)) stopAnimation(animation); + animations.start(animation); } - public void setBorder(V4Color border) { - this.border = border; + public void stopAnimation(VAnimation animation) { + stopAnimation(animation.name); } - public void setBorder(VColor all) { - setBorder(new V4Color(all)); + public void stopAnimation(String animation) { + animations.stop(animation); } - public void setBorder(VColor tb, VColor lr) { - setBorder(new V4Color(tb, lr)); + public void stopAllAnimations() { + for (String animation : animations.getActive()) { + stopAnimation(animation); + } } - public void setBorder(VColor top, VColor bottom, VColor left, VColor right) { - setBorder(new V4Color(top, bottom, left, right)); + public void startOrRewindAnimation(VAnimation animation) { + animations.startOrRewind(animation); } - public V4Int getBorderSize() { - return borderSize; + public void unwindAnimation(VAnimation animation) { + unwindAnimation(animation.name); } - public void setBorderSize(V4Int borderSize) { - this.borderSize = borderSize; + public void unwindAnimation(String animation) { + animations.unwind(animation); } - public void setBorderSize(int all) { - setBorderSize(new V4Int(all)); + public void rewindAnimation(VAnimation animation) { + rewindAnimation(animation.name); } - public void setBorderSize(int tb, int lr) { - setBorderSize(new V4Int(tb, lr)); + public void rewindAnimation(String animation) { + animations.rewind(animation); } - public void setBorderSize(int top, int bottom, int left, int right) { - setBorderSize(new V4Int(top, bottom, left, right)); + public boolean isAnimationActive(VAnimation animation) { + return isAnimationActive(animation.name); } - public void renderBorder() { + public boolean isAnimationActive(String animation) { + return animations.isActive(animation); + } + + public VStyleState createStyleState() { + // Clicks first + if (leftClickDown) return VStyleState.LEFT_CLICKED; + else if (middleClickDown) return VStyleState.MIDDLE_CLICKED; + else if (rightClickDown) return VStyleState.RIGHT_CLICKED; + + else if (DragHandler.isDragging() && DragHandler.target == this) { + return switch (DragHandler.button) { + case LEFT -> VStyleState.LC_DRAGGING; + case MIDDLE -> VStyleState.MC_DRAGGING; + case RIGHT -> VStyleState.RC_DRAGGING; + }; + } + + // Hover as last, since everything else is hover too + else if (isHovered()) return VStyleState.HOVERED; + else return VStyleState.DEFAULT; + } + + public VRenderContext createRenderContext() { + VStyleState state = createStyleState(); + VeraApp app = getApp(); + return new VRenderContext( + app.getX() + getX(), app.getY() + getY(), + getEffectiveWidth(), getEffectiveHeight(), + getStyle("rotation", state), getStyle("scale", state), + hasTransparency + ); + } + + public void renderSelf() { + beforeRender(); + animations.updateLifetimes(); + + if (visibilityConditionsPassed()) { + VRenderContext ctx = createRenderContext(); + Vera.renderer.pushContext(ctx); + + renderContent(ctx); + renderBorder(ctx); + renderOverlay(ctx); + + Vera.renderer.ensureClearContext(ctx); + Vera.renderer.popContext(); + } + + afterRender(); + } + + public void renderBorder(VRenderContext ctx) { + // TODO: [Render Rework] Better border rendering + + VStyleState state = createStyleState(); + + V4Color borderColor = getStyle("border-color", state); + V4Int borderSize = getStyle("border-size", state); + // Top - Vera.renderer.drawRect(app, getHitboxX(), getHitboxY() - borderSize.get1(), getHitboxWidth(), borderSize.get1(), 0, border.get1()); + Vera.renderer.drawRect(ctx, 0, -borderSize.get1(), getEffectiveWidth(), borderSize.get1(), borderColor.get1()); if (borderSize.get3() > 0) { - Vera.renderer.drawRect(app, getHitboxX() - borderSize.get3(), getHitboxY() - borderSize.get1(), borderSize.get3(), borderSize.get1(), 0, border.get1()); + Vera.renderer.drawRect(ctx, -borderSize.get3(), -borderSize.get1(), borderSize.get3(), borderSize.get1(), borderColor.get1()); } // Bottom - Vera.renderer.drawRect(app, getHitboxX(), getHitboxY() + getHitboxHeight(), getHitboxWidth(), borderSize.get2(), 0, border.get2()); + Vera.renderer.drawRect(ctx, 0, getEffectiveHeight(), getEffectiveWidth(), borderSize.get2(), borderColor.get2()); if (borderSize.get4() > 0) { - Vera.renderer.drawRect(app, getHitboxX() + getHitboxWidth(), getHitboxY() + getHitboxHeight(), borderSize.get4(), borderSize.get2(), 0, border.get2()); + Vera.renderer.drawRect(ctx, getEffectiveWidth(), getEffectiveHeight(), borderSize.get4(), borderSize.get2(), borderColor.get2()); } // Left - Vera.renderer.drawRect(app, getHitboxX() - borderSize.get3(), getHitboxY(), borderSize.get3(), getHitboxHeight(), 0, border.get3()); + Vera.renderer.drawRect(ctx, -borderSize.get3(), 0, borderSize.get3(), getEffectiveHeight(), borderColor.get3()); if (borderSize.get2() > 0) { - Vera.renderer.drawRect(app, getHitboxX() - borderSize.get3(), getHitboxY() + getHitboxHeight(), borderSize.get3(), borderSize.get2(), 0, border.get3()); + Vera.renderer.drawRect(ctx, -borderSize.get3(), getEffectiveHeight(), borderSize.get3(), borderSize.get2(), borderColor.get3()); } // Right - Vera.renderer.drawRect(app, getHitboxX() + getHitboxWidth(), getHitboxY(), borderSize.get4(), getHitboxHeight(), 0, border.get4()); + Vera.renderer.drawRect(ctx, getEffectiveWidth(), 0, borderSize.get4(), getEffectiveHeight(), borderColor.get4()); if (borderSize.get1() > 0) { - Vera.renderer.drawRect(app, getHitboxX() + getHitboxWidth(), getHitboxY() - borderSize.get1(), borderSize.get4(), borderSize.get1(), 0, border.get4()); + Vera.renderer.drawRect(ctx, getEffectiveWidth(), -borderSize.get1(), borderSize.get4(), borderSize.get1(), borderColor.get4()); } } - public void setSize(int width, int height) { - setWidth(width); - setHeight(height); - } + public void renderOverlay(VRenderContext ctx) { + VStyleState state = createStyleState(); - public void setSize(int all) { - setSize(all, all); + Vera.renderer.drawRect(ctx, 0, 0, getEffectiveWidth(), getEffectiveHeight(), getStyle("overlay", state)); } - public void move(int x, int y) { - this.x = x; - this.y = y; + public void beforeRender() { + VeraApp app = getApp(); + VStyleState state = createStyleState(); + + if (state != prevStyleState) { + Integer transitionTime = app.styleSheet.getKey(this, "transition", state); + VEasing transitionEasing = app.styleSheet.getKey(this, "transition-easing", state); + + Transition: if (transitionTime > 0) { + if (transitionOrigin == state) { + if (isTransitionUnwinding) rewindAnimation(VAnimation.INTERNAL_TRANSITION_NAME); + else unwindAnimation(VAnimation.INTERNAL_TRANSITION_NAME); + break Transition; + } + + VAnimation.Builder builder = new VAnimation.Builder(VAnimation.INTERNAL_TRANSITION_NAME) + .relativeUnwindTime() + .unwindEasing(transitionEasing); + + builder.keyframe(0, 1, frame -> { + for (String key : app.styleSheet.getKeysStacked(this, prevStyleState)) { + frame.style(key, getStyle(key, prevStyleState)); + } + }); + + builder.keyframe(transitionTime - 1, 0, frame -> { + for (String key : app.styleSheet.getKeysStacked(this, state)) { + frame.style(key, app.styleSheet.getKey(this, key, state)); + } + }); + + transitionOrigin = prevStyleState; + animate(builder.build(), true); + } + + prevStyleState = state; + } } - public void move(int both) { - move(both, both); + public void afterRender() { } public boolean isLeftClickDown() { @@ -197,19 +283,20 @@ public boolean isAnyMouseButtonDown() { return leftClickDown || middleClickDown || rightClickDown; } - public VeraApp getApp() { - return app; + public boolean hasTransparency() { + return hasTransparency; } - public double getRotation() { - return rotation; + public void setHasTransparency(boolean hasTransparency) { + this.hasTransparency = hasTransparency; + events.fire(VEvents.Widget.TRANSPARENCY_STATE_CHANGE, hasTransparency); } - public void rotate(double rotation) { - this.rotation = rotation; - } + public void update() { + VStyleState state = createStyleState(); - public void update() {} + getApp().setCursorShape(getStyle("cursor", state)); + } public boolean isHovered() { return hovered; @@ -218,257 +305,189 @@ public boolean isHovered() { public void setHovered(boolean hovered) { // If changed if (this.hovered != hovered) { - if (hovered) fireEvent("hover"); - else fireEvent("hover-leave"); + if (hovered) events.fire(VEvents.Widget.HOVER); + else events.fire(VEvents.Widget.HOVER_LEAVE); } this.hovered = hovered; } public void onHover(Runnable runnable) { - registerEventExecutor("hover", runnable); + events.register(VEvents.Widget.HOVER, runnable); } public void onHoverLeave(Runnable runnable) { - registerEventExecutor("hover-leave", runnable); + events.register(VEvents.Widget.HOVER_LEAVE, runnable); } public void onLeftClick(Runnable runnable) { - registerEventExecutor("left-click", runnable); + events.register(VEvents.Widget.LEFT_CLICK, runnable); } public void onLeftClickRelease(Runnable runnable) { - registerEventExecutor("left-click-release", runnable); + events.register(VEvents.Widget.LEFT_CLICK_RELEASE, runnable); } public void onRightClick(Runnable runnable) { - registerEventExecutor("right-click", runnable); + events.register(VEvents.Widget.RIGHT_CLICK, runnable); } public void onRightClickRelease(Runnable runnable) { - registerEventExecutor("right-click-release", runnable); + events.register(VEvents.Widget.RIGHT_CLICK_RELEASE, runnable); } public void onMiddleClick(Runnable runnable) { - registerEventExecutor("middle-click", runnable); + events.register(VEvents.Widget.MIDDLE_CLICK, runnable); } public void onMiddleClickRelease(Runnable runnable) { - registerEventExecutor("middle-click-release", runnable); + events.register(VEvents.Widget.MIDDLE_CLICK_RELEASE, runnable); } public void onMouseScroll(VMouseScrollEvent runnable) { - registerEventExecutor("mouse-scroll", args -> runnable.run( + events.register(VEvents.Widget.SCROLL, args -> runnable.run( (int) args[0], (int) args[1], (double) args[2]) ); } public void onMouseMove(VMouseMoveEvent runnable) { - registerEventExecutor("mouse-move", args -> runnable.run((int) args[0], (int) args[1])); + events.register(VEvents.Widget.MOUSE_MOVE, args -> runnable.run((int) args[0], (int) args[1])); } public void onMouseDragLeft(VMouseDragEvent runnable) { - registerEventExecutor("mouse-drag-left", args -> runnable.run((int) args[0], (int) args[1], (int) args[2], (int) args[3])); + events.register(VEvents.Widget.DRAG_LEFT_CLICK, args -> runnable.run((VMouseDragEvent.Context) args[0])); } public void onMouseDragRight(VMouseDragEvent runnable) { - registerEventExecutor("mouse-drag-right", args -> runnable.run((int) args[0], (int) args[1], (int) args[2], (int) args[3])); + events.register(VEvents.Widget.DRAG_RIGHT_CLICK, args -> runnable.run((VMouseDragEvent.Context) args[0])); } public void onMouseDragMiddle(VMouseDragEvent runnable) { - registerEventExecutor("mouse-drag-middle", args -> runnable.run((int) args[0], (int) args[1], (int) args[2], (int) args[3])); + events.register(VEvents.Widget.DRAG_MIDDLE_CLICK, args -> runnable.run((VMouseDragEvent.Context) args[0])); } public void onFocusStateChange(Runnable runnable) { - registerEventExecutor("focus-state-change", runnable); + events.register(VEvents.Widget.FOCUS_STATE_CHANGE, runnable); } public void onFilesDropped(VFilesDroppedEvent runnable) { - registerEventExecutor("files-dropped", args -> runnable.run((List) args[0])); + events.register(VEvents.Widget.FILES_DROPPED, args -> runnable.run((List) args[0])); } - public void onMessage(VWidgetMessageEvent runnable) { - registerEventExecutor("widget-message", args -> runnable.run((VWidgetMessageEvent.Context) args[0])); + public void onAnimationBegin(VAnimationBeginEvent runnable) { + events.register(VEvents.Animation.BEGIN, args -> runnable.run((VAnimation) args[0])); } - public void sendMessage(VWidget widget, String type) { - sendMessage(widget, type, null); + public void onAnimationUnwindBegin(VAnimationUnwindEvent runnable) { + events.register(VEvents.Animation.UNWIND_BEGIN, args -> runnable.run((VAnimation) args[0])); } - public void sendMessage(VWidget widget, String type, @Nullable Object content) { - widget.fireEvent("widget-message", new VWidgetMessageEvent.Context(this, type, content)); + public void onAnimationRewindBegin(VAnimationRewindEvent runnable) { + events.register(VEvents.Animation.REWIND_BEGIN, args -> runnable.run((VAnimation) args[0])); } - public void sendMessageAll(String type) { - sendMessageAll(type, null); + public void onAnimationFinish(VAnimationFinishEvent runnable) { + events.register(VEvents.Animation.FINISH, args -> runnable.run((VAnimation) args[0], (long) args[1])); } - public void sendMessageAll(String type, @Nullable Object content) { - VWidgetMessageEvent.Context ctx = new VWidgetMessageEvent.Context(this, type, content); - for (VWidget widget : app.getWidgets()) widget.fireEvent("widget-message", ctx); - } - - public boolean isVisible() { - return visible; - } - - public void setVisible(boolean visible) { - this.visible = visible; - } - - public void show() { - setVisible(true); - } - - public void hide() { - setVisible(false); - } - - public void registerEventExecutor(String event, VEvent executor) { - eventExecutors.computeIfAbsent(event, k -> new ArrayList<>()).add(executor); - } - - public void registerEventExecutor(String event, Runnable runnable) { - registerEventExecutor(event, args -> runnable.run()); - } - - public void fireEvent(String event, Object... args) { - handleBuiltinEvent(event, args); - - if (!eventExecutors.containsKey(event)) return; - eventExecutors.get(event).parallelStream().forEach(e -> e.run(args)); - } - - public void clearEvents() { - eventExecutors.clear(); - } - - public void clearEventsFor(String event) { - // IDE said I don't need a containsKey check - eventExecutors.remove(event); + public void onTransparencyStateChange(VTransparencyStateChangeEvent runnable) { + events.register(VEvents.Widget.TRANSPARENCY_STATE_CHANGE, args -> runnable.run((boolean) args[0])); } + @Override public void handleBuiltinEvent(String event, Object... args) { switch (event) { - case "left-click" -> { - if (shouldFocusOnClick()) { + case VEvents.Widget.LEFT_CLICK -> { + if (focusOnClick) { setFocused(true); } leftClickDown = true; } - case "right-click" -> rightClickDown = true; - case "middle-click" -> middleClickDown = true; - - case "left-click-release" -> clearLeftClickDown(); - case "right-click-release" -> clearRightClickDown(); - case "middle-click-release" -> clearMiddleClickDown(); - - case "mouse-move" -> { - if (leftClickDown) { - int newX = (int) args[0]; - int newY = (int) args[1]; - if (leftDragPreviousX != -1 || leftDragPreviousY != -1) fireEvent("mouse-drag-left", leftDragPreviousX, leftDragPreviousY, newX, newY); - - leftDragPreviousX = newX; - leftDragPreviousY = newY; - } else if (rightClickDown) { - int newX = (int) args[0]; - int newY = (int) args[1]; - - if (rightDragPreviousX != -1 || rightDragPreviousY != -1) fireEvent("mouse-drag-right", rightDragPreviousX, rightDragPreviousY, newX, newY); - - rightDragPreviousX = newX; - rightDragPreviousY = newY; - } else if (middleClickDown) { - int newX = (int) args[0]; - int newY = (int) args[1]; - - if (middleDragPreviousX != -1 || middleDragPreviousY != -1) fireEvent("mouse-drag-middle", middleDragPreviousX, middleDragPreviousY, newX, newY); - - middleDragPreviousX = newX; - middleDragPreviousY = newY; - } - } + case VEvents.Widget.RIGHT_CLICK -> rightClickDown = true; + case VEvents.Widget.MIDDLE_CLICK -> middleClickDown = true; - case "hover" -> { - cursorBeforeHover = app.getCursorShape(); - app.setCursorShape(hoverCursor); - } + case VEvents.Widget.LEFT_CLICK_RELEASE -> clearLeftClickDown(); + case VEvents.Widget.RIGHT_CLICK_RELEASE -> clearRightClickDown(); + case VEvents.Widget.MIDDLE_CLICK_RELEASE -> clearMiddleClickDown(); - case "hover-leave" -> { + case VEvents.Widget.HOVER_LEAVE -> { clearLeftClickDown(); clearRightClickDown(); clearMiddleClickDown(); + } - if (cursorBeforeHover == null) break; - - app.setCursorShape(cursorBeforeHover); + case VEvents.Animation.FINISH -> { + CompiledAnimation animation = (CompiledAnimation) args[0]; + if (animation.name.equals(VAnimation.INTERNAL_TRANSITION_NAME)) { + transitionOrigin = null; + isTransitionUnwinding = false; + } } + case VEvents.Animation.UNWIND_BEGIN -> isTransitionUnwinding = true; + case VEvents.Animation.REWIND_BEGIN -> isTransitionUnwinding = false; + } + } + + @Override + public void afterBuiltinEvent(String name, Object... args) { + updateIfNeeded(); + } + + private void updateIfNeeded() { + VStyleState state = createStyleState(); + if (state != handledPrevStyleState) { + update(); + handledPrevStyleState = state; } } private void clearLeftClickDown() { leftClickDown = false; - leftDragPreviousX = -1; - leftDragPreviousY = -1; } private void clearRightClickDown() { rightClickDown = false; - rightDragPreviousX = -1; - rightDragPreviousY = -1; } private void clearMiddleClickDown() { middleClickDown = false; - middleDragPreviousX = -1; - middleDragPreviousY = -1; - } - - public VCursorShape getHoverCursor() { - return hoverCursor; - } - - public void setHoverCursor(@Nullable VCursorShape hoverCursor) { - this.hoverCursor = hoverCursor == null ? VCursorShape.DEFAULT : hoverCursor; } public boolean isFocused() { - return app.isFocusedWidget(this); + return getApp().isFocusedWidget(this); } public void setFocused(boolean focused) { + VeraApp app = getApp(); + if (focused) app.setFocusedWidget(this); else app.setFocusedWidget(null); } - public boolean shouldFocusOnClick() { - return focusOnClick; - } - - public void setFocusOnClick(boolean focus) { - focusOnClick = focus; - } - public void keyPressed(int keyCode, int scanCode, int modifiers) {} public void charTyped(char chr, int modifiers) {} public void remove() { - app.removeWidget(this); + appAccess.removeWidget(this); } - public T alsoAdd() { - app.addWidget(this); + public T alsoAddClass(String clazz) { + classes.add(clazz); return (T) this; } - public void addVisibilityCondition(Supplier condition) { - visibilityConditions.add(condition); + public T alsoAdd() { + appAccess.addWidget(this); + return (T) this; } - public boolean visibilityConditionsPassed() { - return visibilityConditions.parallelStream().allMatch(Supplier::get); + @Override + public T alsoAddTo(VLayout layout) { + super.alsoAddTo(layout); + alsoAdd(); + + return (T) this; } } diff --git a/src/main/resources/assets/mcvera/demo/test.vss b/src/main/resources/assets/mcvera/demo/test.vss new file mode 100644 index 00000000..1ffee70b --- /dev/null +++ b/src/main/resources/assets/mcvera/demo/test.vss @@ -0,0 +1,23 @@ +VeraApp { + background-color: (0 0 0 0.40); +} + +VLabel { + color: black; + background-color: white; + + padding: 5px 15px; + border: 1px (0 0 0); + border-opacity: 0.5; + + cursor: pointer; + transition: 0.1s; +} + +VLabel:hover { + background-filter: "sub" 20; +} + +VLabel:clicked { + background-filter: "sub" 40; +} diff --git a/src/main/resources/assets/mcvera/icon.png b/src/main/resources/assets/mcvera/icon.png index 43e7c1fd..48341f58 100644 Binary files a/src/main/resources/assets/mcvera/icon.png and b/src/main/resources/assets/mcvera/icon.png differ diff --git a/src/main/resources/mcvera.mixins.json b/src/main/resources/mcvera.mixins.json index 79f4f793..2288d01b 100644 --- a/src/main/resources/mcvera.mixins.json +++ b/src/main/resources/mcvera.mixins.json @@ -3,13 +3,17 @@ "package": "net.snackbag.mcvera.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ - "MinecraftClientMixin", - "VeraAppMixin" + "MinecraftClientMixin" ], "injectors": { "defaultRequire": 1 }, "client": [ + "DrawContextAccessor", + + "DrawContextMixin", + "GameRendererMixin", + "InGameHudMixin", "KeyboardMixin", "MouseMixin", "ParentElementMixin",