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/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/net/snackbag/mcvera/InternalCommands.java b/src/main/java/net/snackbag/mcvera/InternalCommands.java new file mode 100644 index 00000000..ac16ce0d --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/InternalCommands.java @@ -0,0 +1,45 @@ +package net.snackbag.mcvera; + +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.LayoutTestApplication; +import net.snackbag.mcvera.test.StyleTestApplication; +import net.snackbag.mcvera.test.TestApplication; + +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("clear-tests") + .executes((ctx) -> { + TestApplication.INSTANCE = new TestApplication(); + StyleTestApplication.INSTANCE = new StyleTestApplication(); + LayoutTestApplication.INSTANCE = new LayoutTestApplication(); + return 1; + }) + ) + ); + } +} diff --git a/src/main/java/net/snackbag/mcvera/MCVeraData.java b/src/main/java/net/snackbag/mcvera/MCVeraData.java index 663b6299..f75b54c2 100644 --- a/src/main/java/net/snackbag/mcvera/MCVeraData.java +++ b/src/main/java/net/snackbag/mcvera/MCVeraData.java @@ -1,17 +1,37 @@ package net.snackbag.mcvera; import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.flag.VWindowPositioningFlag; +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 List appHierarchy = new ArrayList<>(); + + 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 (appHierarchy.isEmpty()) return false; + + runnable.accept(appHierarchy.get(0)); + return true; + } + + public static @Nullable VeraApp getTopHierarchy() { + return appHierarchy.isEmpty() ? null : appHierarchy.get(0); + } } diff --git a/src/main/java/net/snackbag/mcvera/MinecraftVera.java b/src/main/java/net/snackbag/mcvera/MinecraftVera.java index 6a4262f1..66caf1e1 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,17 @@ 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 ImageStandardStyle()); + 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..1555088a 100644 --- a/src/main/java/net/snackbag/mcvera/impl/MCVeraProvider.java +++ b/src/main/java/net/snackbag/mcvera/impl/MCVeraProvider.java @@ -7,28 +7,26 @@ import net.snackbag.vera.Vera; import net.snackbag.vera.core.VFont; import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.Events; import net.snackbag.vera.event.VShortcut; 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); + MCVeraData.visibleApplications.get(app.getPositioning()).add(app); if (app.isMouseRequired()) MCVeraData.appsWithMouseRequired += 1; MinecraftClient client = MinecraftClient.getInstance(); client.send(app::update); @@ -44,7 +42,7 @@ public void handleAppHide(VeraApp app) { if (!app.isVisible()) return; if (app.isMouseRequired()) MCVeraData.appsWithMouseRequired -= 1; - MCVeraData.visibleApplications.remove(app); + 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(Events.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(Events.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..5859dd22 100644 --- a/src/main/java/net/snackbag/mcvera/impl/MCVeraRenderer.java +++ b/src/main/java/net/snackbag/mcvera/impl/MCVeraRenderer.java @@ -8,36 +8,33 @@ import net.minecraft.text.Text; import net.minecraft.util.Identifier; import net.minecraft.util.math.RotationAxis; +import net.snackbag.mcvera.MCVeraData; +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.flag.VWindowPositioningFlag; import net.snackbag.vera.widget.VWidget; +import org.lwjgl.opengl.GL11; +import java.util.ArrayList; +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(); - } - - return instance; - } - public void drawRect(VeraApp app, int x, int y, int width, int height, double rotation, VColor color) { MatrixStack stack = drawContext.getMatrices(); stack.push(); - int centerX = x + width / 2; - int centerY = y + height / 2; + float centerX = x + width / 2f; + float centerY = y + height / 2f; 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); + stack.translate(-width / 2f, -height / 2f, 0); drawContext.fill(0, 0, width, height, color.toInt()); @@ -66,20 +63,45 @@ public void drawImage(VeraApp app, int x, int y, int width, int height, double r } 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.getTopWidgetAt(Vera.getMouseX(), Vera.getMouseY()); for (VWidget widget : widgets) { - widget.setHovered(hoveredWidgets.contains(widget)); + if (widget != hoveredWidget && widget.isHovered()) widget.setHovered(false); + else if (widget == hoveredWidget && !widget.isHovered()) widget.setHovered(true); + + widget.animations.update(); + if (widget.visibilityConditionsPassed()) { widget.render(); widget.renderBorder(); + widget.renderOverlay(); } } app.renderAfterWidgets(); - RenderSystem.disableBlend(); + if (!blendEnabled) RenderSystem.disableBlend(); + } + + public void renderApps(VWindowPositioningFlag flag) { + LinkedHashSet apps = MCVeraData.visibleApplications.getOrDefault(flag, new LinkedHashSet<>()); + List hierarchicApps = new ArrayList<>(); + + for (VeraApp app : apps) { + if (app.isRequiresHierarchy()) { + hierarchicApps.add(app); + continue; + } + + Vera.renderer.renderApp(app); + } + + for (VeraApp app : hierarchicApps) { + Vera.renderer.renderApp(app); + } } } 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..e86e3e00 --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/mixin/GameRendererMixin.java @@ -0,0 +1,35 @@ +package net.snackbag.mcvera.mixin; + +import net.minecraft.client.render.GameRenderer; +import net.snackbag.vera.Vera; +import net.snackbag.vera.flag.VWindowPositioningFlag; +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.LinkedHashSet; + +@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(VWindowPositioningFlag.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(VWindowPositioningFlag.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(VWindowPositioningFlag.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(VWindowPositioningFlag.SCREEN); + Vera.renderer.renderApps(VWindowPositioningFlag.TOP); + } +} 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..748fae32 --- /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.VWindowPositioningFlag; +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(VWindowPositioningFlag.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(VWindowPositioningFlag.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(VWindowPositioningFlag.BELOW_HUD); + } + + @Inject(at = @At(value = "TAIL"), method = "renderHotbar") + private void mcvera$renderHud(float tickDelta, DrawContext context, CallbackInfo ci) { + Vera.renderer.renderApps(VWindowPositioningFlag.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..507c34a4 100644 --- a/src/main/java/net/snackbag/mcvera/mixin/MouseMixin.java +++ b/src/main/java/net/snackbag/mcvera/mixin/MouseMixin.java @@ -3,6 +3,11 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.Mouse; import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VCursorShape; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.Events; +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 +28,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 = Vera.getTopHierarchyApp(); + Vera.forAllVisibleApps(app -> { + if (app.isRequiresHierarchy() && app != top) return; + + VWidget widget = app.getTopWidgetAt(mouseX, mouseY); + if (widget != null) widget.events.fire(Events.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..1fb85bee 100644 --- a/src/main/java/net/snackbag/mcvera/mixin/ParentElementMixin.java +++ b/src/main/java/net/snackbag/mcvera/mixin/ParentElementMixin.java @@ -1,8 +1,16 @@ 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.Events; +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; @@ -10,32 +18,99 @@ @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); + + VeraApp top = MCVeraData.getTopHierarchy(); + + for (VeraApp app : MCVeraData.appHierarchy) { + if (app.isPointOverThis(mouseX, mouseY) && top != app) { + 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.isRequiresHierarchy()) 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(Events.Widget.LEFT_CLICK); + case RIGHT -> widget.events.fire(Events.Widget.RIGHT_CLICK); + case MIDDLE -> widget.events.fire(Events.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.isRequiresHierarchy()) 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(Events.Widget.LEFT_CLICK_RELEASE); + case RIGHT -> widget.events.fire(Events.Widget.RIGHT_CLICK_RELEASE); + case MIDDLE -> widget.events.fire(Events.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.isRequiresHierarchy()) 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(Events.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/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/StyleTestApplication.java b/src/main/java/net/snackbag/mcvera/test/StyleTestApplication.java new file mode 100644 index 00000000..6902b6b9 --- /dev/null +++ b/src/main/java/net/snackbag/mcvera/test/StyleTestApplication.java @@ -0,0 +1,70 @@ +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.StyleState; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.style.animation.LoopMode; +import net.snackbag.vera.style.animation.VAnimation; +import net.snackbag.vera.style.animation.easing.Easings; +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 testAnimation = new VAnimation.Builder(this, "test") + .unwindTime(2000) + .unwindOnFinish() + .loop(LoopMode.REPEAT) + + .keyframe(1000, frame -> frame.style("background-color", VColor.MC_GOLD), 2000) + .keyframe(1000, frame -> frame.style("background-color", VColor.MC_RED), 2000) + .build(); + + private final VAnimation hoverAnimation = new VAnimation.Builder(this, "hover") + .unwindTime(1000) + .keepFinalStyle(StyleState.HOVERED) + + .keyframe(1000, frame -> frame.style("background-color", VColor.MC_WHITE), 0) + .build(); + + @Override + public void init() { + new VShortcut(this, "escape", this::hide); + + mergeStyleSheet(createStyleSheet()); + + new VLabel("helo", this) + .alsoAddClass("label") + .alsoAdd(); + + VRect testRect = new VRect(VColor.black(), this).alsoAdd(); +// testRect.onHover(() -> testRect.animations.activateOrRewind(hoverAnimation)); +// testRect.onHoverLeave(() -> testRect.animations.activate(hoverAnimation)); + + testRect.setStyle("cursor", StyleState.HOVERED, VCursorShape.POINTING_HAND); + testRect.setStyle("cursor", StyleState.CLICKED, VCursorShape.ALL_RESIZE); + testRect.setStyle("background-color", StyleState.HOVERED, VColor.white()); + testRect.setStyle("transition", StyleState.HOVERED, 1000); + + testRect.onMouseDragLeft((ctx) -> testRect.move(testRect.getX() + ctx.moveX(), testRect.getY() + ctx.moveY())); + + new VShortcut(this, "a", () -> testRect.animations.activate(testAnimation)); + new VShortcut(this, "u", () -> testRect.animations.unwind(testAnimation)); + new VShortcut(this, "r", () -> testRect.animations.rewind(testAnimation)); + new VShortcut(this, "k", () -> testRect.animations.kill(testAnimation)); + } + + public VStyleSheet createStyleSheet() { + VStyleSheet sheet = new VStyleSheet(); + + sheet.setKey("label", "font", VFont.create()); + sheet.setKey("label", "font", VFont.create().withColor(VColor.MC_GOLD), StyleState.HOVERED); + + 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..80c84255 100644 --- a/src/main/java/net/snackbag/mcvera/test/TestApplication.java +++ b/src/main/java/net/snackbag/mcvera/test/TestApplication.java @@ -1,24 +1,20 @@ 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.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.style.StyleState; 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(); public TestApplication() { super(); @@ -26,7 +22,7 @@ public TestApplication() { @Override public void init() { - VShortcut exit = new VShortcut(this, "escape", () -> { + new VShortcut(this, "escape", () -> { if (hasFocusedWidget()) { setFocusedWidget(null); return; @@ -35,13 +31,10 @@ public void init() { this.hide(); }); - VShortcut changeMouseRequired = new VShortcut(this, "leftalt+m", () -> { + new VShortcut(this, "leftalt+m", () -> { setMouseRequired(!isMouseRequired()); }); - addShortcut(exit); - addShortcut(changeMouseRequired); - VLineInput input = new VLineInput(this).alsoAdd(); input.setMaxChars(15); input.setPlaceholderText("Enter text..."); @@ -49,23 +42,23 @@ public void init() { input.onMouseMove((x, y) -> System.out.println("x=" + x + ", y=" + y)); input.move(50); - input.setBackgroundColor(VColor.white()); + input.setStyle("background-color", 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-color", VColor.black()); + label.modifyFont().color(VColor.white()); label.adjustSize(); label.onHover(() -> { label.setText("Hovered"); @@ -75,30 +68,29 @@ 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-color", 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-color", 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"), + "minecraft:textures/block/dirt.png", 32, 32, this).alsoAdd(); image.move(0, 30); image.onMiddleClick(this::hideCursor); image.onMiddleClickRelease(this::showCursor); + image.setStyle("src", StyleState.HOVERED, "minecraft:textures/block/diamond_block.png"); VDropdown dropdown = new VDropdown(this).alsoAdd(); dropdown.addItem("coolio"); @@ -107,7 +99,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 +107,7 @@ public void init() { VCheckBox checkbox = new VCheckBox(this).alsoAdd(); checkbox.move(20, 140); - checkbox.setHoverOverlayColor(VColor.white().withOpacity(0.4f)); + checkbox.setStyle("overlay", StyleState.HOVERED, VColor.white().withOpacity(0.4f)); checkbox.onCheckStateChange((state) -> { if (!state) removeWidget(checkbox); 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/VElement.java b/src/main/java/net/snackbag/vera/VElement.java new file mode 100644 index 00000000..8ac7b39e --- /dev/null +++ b/src/main/java/net/snackbag/vera/VElement.java @@ -0,0 +1,156 @@ +package net.snackbag.vera; + +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.EventHandler; +import net.snackbag.vera.event.Events; +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 VeraApp app; + private final List> visibilityConditions = new ArrayList<>(); + + protected @Nullable VLayout layout; + + public VElement(VeraApp app, int x, int y, int width, int height) { + this.app = 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 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(Events.Element.MESSAGE,args -> executor.run((VWidgetMessageEvent.Context) args[0])); + } + + public void sendMessage(VElement element, String type, @Nullable Object content) { + element.events.fire(Events.Element.MESSAGE, new VWidgetMessageEvent.Context(this, type, content)); + } + + public void onLayoutSwap(Consumer executor) { + events.register(Events.Element.LAYOUT_SWAP, args -> executor.accept((VLayout) args[0])); + } + + public void onLayoutRemove(Runnable executor) { + events.register(Events.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 int getEffectiveX() { + return getX(); + } + + public int getEffectiveY() { + return 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..58136e15 100644 --- a/src/main/java/net/snackbag/vera/Vera.java +++ b/src/main/java/net/snackbag/vera/Vera.java @@ -3,47 +3,55 @@ 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.VWindowPositioningFlag; 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; - for (VWidget widget : hoveredWidgets) { - if (widget.visibilityConditionsPassed()) runnable.accept(widget); - } + public static void forVisibleAndAllowedApps(Consumer handler) { + final List handledApps = new ArrayList<>(); + if (!MCVeraData.appHierarchy.isEmpty()) { + VeraApp app = MCVeraData.appHierarchy.get(0); + + handledApps.add(app); + handler.accept(app); } - } - public static void forHoveredWidget(int mouseX, int mouseY, Consumer> runnable) { - forHoveredWidget(mouseX, mouseY, runnable, null); + for (VWindowPositioningFlag flag : MCVeraData.visibleApplications.keySet()) { + for (VeraApp app : MCVeraData.visibleApplications.get(flag)) { + if (handledApps.contains(app) || app.isRequiresHierarchy()) 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 (VWindowPositioningFlag flag : MCVeraData.visibleApplications.keySet()) { + for (VeraApp app : MCVeraData.visibleApplications.get(flag)) { + handler.accept(app); + } } } @@ -76,4 +84,22 @@ public static int getMouseY() { return path; } } + + public static @Nullable VeraApp getTopHierarchyApp() { + return MCVeraData.appHierarchy.isEmpty() ? null : MCVeraData.appHierarchy.get(0); + } + + public static boolean isTopHierarchy(VeraApp app) { + return getTopHierarchyApp() == app; + } + + @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; + } } 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/VColor.java b/src/main/java/net/snackbag/vera/core/VColor.java index d3b2da74..ab5a807f 100644 --- a/src/main/java/net/snackbag/vera/core/VColor.java +++ b/src/main/java/net/snackbag/vera/core/VColor.java @@ -1,5 +1,8 @@ package net.snackbag.vera.core; +import net.snackbag.vera.style.animation.easing.VEasing; +import org.jetbrains.annotations.ApiStatus; + import java.util.function.Consumer; public class VColor { @@ -89,19 +92,43 @@ public boolean isTransparent() { return opacity == 0; } + @Deprecated(since = "1.10", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "1.11") 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 = "1.10", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "1.11") 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 = "1.10", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "1.11") 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 = "1.10", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "1.11") 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); } @@ -156,6 +183,15 @@ 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)); } + public VColor ease(VEasing easing, VColor target, float delta) { + 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) + ); + } + public static VColor transparent() { return new VColor(0, 0, 0, 0); } @@ -189,6 +225,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/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/VeraApp.java b/src/main/java/net/snackbag/vera/core/VeraApp.java index 0e218746..747e5554 100644 --- a/src/main/java/net/snackbag/vera/core/VeraApp.java +++ b/src/main/java/net/snackbag/vera/core/VeraApp.java @@ -1,19 +1,26 @@ package net.snackbag.vera.core; import net.minecraft.client.MinecraftClient; +import net.snackbag.mcvera.MCVeraData; import net.snackbag.vera.Vera; +import net.snackbag.vera.event.Events; import net.snackbag.vera.event.VShortcut; +import net.snackbag.vera.flag.VWindowPositioningFlag; +import net.snackbag.vera.style.VStyleSheet; +import net.snackbag.vera.style.animation.VeraPipeline; +import net.snackbag.vera.style.animation.composite.AnimationComposite; +import net.snackbag.vera.style.animation.composite.WindingComposite; +import net.snackbag.vera.util.Geometry; import net.snackbag.vera.widget.VWidget; 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 { + public final VStyleSheet styleSheet = new VStyleSheet(); + public final VeraPipeline pipeline = new VeraPipeline(this); + private final List> widgets; private final HashMap shortcuts; private VColor backgroundColor; @@ -27,7 +34,9 @@ public abstract class VeraApp { private int height; private boolean visible; + private boolean requiresHierarchy; private @Nullable VWidget focusedWidget; + private VWindowPositioningFlag positioning; public VeraApp() { this(true); @@ -49,6 +58,14 @@ public VeraApp(boolean mouseRequired) { this.y = 0; this.visible = false; + setPositioning(VWindowPositioningFlag.SCREEN); + + loadComposites(); + } + + public void loadComposites() { + pipeline.addPass(new AnimationComposite()); + pipeline.addPass(new WindingComposite()); } public void setCursorVisible(boolean cursorVisible) { @@ -138,6 +155,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,13 +181,41 @@ public int getY() { return y; } + public void setRequiresHierarchy(boolean requires) { + if (MCVeraData.appHierarchy.contains(this) && !requires) { + MCVeraData.appHierarchy.remove(this); + } + + MCVeraData.appHierarchy.add(this); + this.requiresHierarchy = requires; + } + + public void moveToHierarchyTop() { + if (!requiresHierarchy) return; + + MCVeraData.appHierarchy.remove(this); + MCVeraData.appHierarchy.add(0, this); + } + + public boolean isRequiresHierarchy() { + return requiresHierarchy; + } + public abstract void init(); public List> getWidgets() { return new ArrayList<>(widgets); } + public List> getWidgetsReversed() { + List> widgets = getWidgets(); + Collections.reverse(widgets); + + return widgets; + } + public void addWidget(VWidget widget) { + if (widgets.contains(widget)) return; this.widgets.add(widget); } @@ -169,10 +223,10 @@ public void removeWidget(VWidget widget) { if (!widgets.contains(widget)) 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(Events.Widget.LEFT_CLICK_RELEASE); + if (widget.isMiddleClickDown()) widget.events.fire(Events.Widget.MIDDLE_CLICK_RELEASE); + if (widget.isRightClickDown()) widget.events.fire(Events.Widget.RIGHT_CLICK_RELEASE); + if (widget.isHovered()) widget.events.fire(Events.Widget.HOVER_LEAVE); this.widgets.remove(widget); } @@ -210,31 +264,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 widgetWidth = widget.getHitboxWidth(); int widgetHeight = widget.getHitboxHeight(); - return mouseX >= widgetX && mouseX <= widgetX + widgetWidth && - mouseY >= widgetY && mouseY <= widgetY + widgetHeight; + return Geometry.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 Geometry.isInBox(px, py, x, y, width, height); } public void setFocusedWidget(@Nullable VWidget widget) { @@ -242,8 +295,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(Events.Widget.FOCUS_STATE_CHANGE); + if (widget != null) widget.events.fire(Events.Widget.FOCUS_STATE_CHANGE); } } @@ -273,6 +326,27 @@ public void setCursorShape(VCursorShape cursorShape) { ); } + public VWindowPositioningFlag getPositioning() { + return positioning; + } + + public void setPositioning(VWindowPositioningFlag 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 +354,8 @@ 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); + } } 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/Events.java b/src/main/java/net/snackbag/vera/event/Events.java new file mode 100644 index 00000000..f79f09a0 --- /dev/null +++ b/src/main/java/net/snackbag/vera/event/Events.java @@ -0,0 +1,73 @@ +package net.snackbag.vera.event; + +// Sorted by :sparkle: the feeling that it looks nice :sparkle: +public class Events { + // 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 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/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/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..81848dd5 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 1.10, will be removed in 1.11. No longer needed since the app now already receives the + * shortcut on shortcut initialization. + */ + @Deprecated(forRemoval = true, since = "1.10") + @ApiStatus.ScheduledForRemoval(inVersion = "1.11") public VShortcut alsoAdd() { app.addShortcut(this); return this; 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/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/flag/VWindowFlag.java b/src/main/java/net/snackbag/vera/flag/VWindowFlag.java new file mode 100644 index 00000000..3ab843d8 --- /dev/null +++ b/src/main/java/net/snackbag/vera/flag/VWindowFlag.java @@ -0,0 +1,5 @@ +package net.snackbag.vera.flag; + +public enum VWindowFlag { + DEBUG +} diff --git a/src/main/java/net/snackbag/vera/flag/VWindowPositioningFlag.java b/src/main/java/net/snackbag/vera/flag/VWindowPositioningFlag.java new file mode 100644 index 00000000..0ddebd8b --- /dev/null +++ b/src/main/java/net/snackbag/vera/flag/VWindowPositioningFlag.java @@ -0,0 +1,53 @@ +package net.snackbag.vera.flag; + +public enum VWindowPositioningFlag { + /** + * 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/layout/VHLayout.java b/src/main/java/net/snackbag/vera/layout/VHLayout.java new file mode 100644 index 00000000..7379df62 --- /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.VeraApp; +import org.joml.Vector2i; + +public class VHLayout extends VLayout { + public VHLayout(VeraApp app, int x, int y, int width, int height) { + super(app, x, y, width, height); + } + + public VHLayout(VeraApp app, int x, int y) { + this(app, x, y, -1, -1); + } + + public VHLayout(VLayout parent, int width, int height) { + this(parent.app, 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(getWidth() / 2 - calculateElementsWidth() / 2 + original.x, original.y); + case END -> new Vector2i(getWidth() - 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..c7943d8d --- /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.VeraApp; +import net.snackbag.vera.event.Events; +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(VeraApp 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(Events.Element.LAYOUT_SWAP, this); + } + + public boolean removeElement(VElement elem) { + if (!elements.contains(elem)) return false; + + elem.events.fire(Events.Element.LAYOUT_REMOVE); + return elements.remove(elem); + } + + public void clear() { + for (VElement elem : elements) elem.events.fire(Events.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..9dba13bc --- /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.VeraApp; +import org.joml.Vector2i; + +public class VVLayout extends VLayout { + public VVLayout(VeraApp app, int x, int y, int width, int height) { + super(app, x, y, width, height); + } + + public VVLayout(VeraApp app, int x, int y) { + this(app, x, y, -1, -1); + } + + public VVLayout(VLayout parent, int width, int height) { + this(parent.app, 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, getHeight() / 2 - calculateElementsHeight() / 2 + original.y); + case END -> new Vector2i(original.x, getHeight() - calculateElementsHeight() + original.y); + }; + } +} 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..677c24eb --- /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.StyleState; +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 StyleState state) { + return getApp().styleSheet.modifyKeyAsFont((VWidget) this, "font", state); + } + + default VColor.ColorModifier modifyFontColor() { + return modifyFontColor(null); + } + + default VColor.ColorModifier modifyFontColor(@Nullable StyleState 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..3067b287 --- /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.StyleState; +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 StyleState state) { + return getApp().styleSheet.modifyKeyAsFont((VWidget) this, "placeholder-font", state); + } + + default VColor.ColorModifier modifyPlaceholderFontColor() { + return modifyPlaceholderFontColor(null); + } + + default VColor.ColorModifier modifyPlaceholderFontColor(@Nullable StyleState 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..266cac8e 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 = "1.11") +@Deprecated(forRemoval = true, since = "1.10") 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..8a3ef522 --- /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
+ * |------ StyleState: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, StyleState state) { + return getKey(part, key).containsKey(state); + } + + public V getState(T part, String key, StyleState 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, StyleState)} + * for deeper state resolve. + *

+ * For example when requesting state HOVERED: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
KeyStateReturned
src + * DEFAULT + * No + *
overlay + * HOVERED + * Yes + *
fontCLICKEDNo
+ * + * @see #getKeysStacked(Object, StyleState) + */ + public Set getKeysExact(T part, @Nullable StyleState state) { + if (state == null) state = StyleState.DEFAULT; + + Set buffer = new HashSet<>(); + + var resolvedPart = getPart(part); // i'm sorry for using var but holy fuck + for (String key : resolvedPart.keySet()) { + for (StyleState 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, StyleState)} 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, StyleState) + */ + public Set getKeysStacked(T part, @Nullable StyleState state) { + if (state == null) state = StyleState.DEFAULT; + + Set buffer = new HashSet<>(); + + StyleState next = state; + while (next != null) { + buffer.addAll(getKeysExact(part, next)); + next = next.fallback; + } + + return buffer; + } + + public void put(T part, String key, StyleState 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 (StyleState 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/StyleState.java b/src/main/java/net/snackbag/vera/style/StyleState.java new file mode 100644 index 00000000..7c180e1d --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/StyleState.java @@ -0,0 +1,54 @@ +package net.snackbag.vera.style; + +import org.jetbrains.annotations.Nullable; + +public enum StyleState { + 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 StyleState fallback; + + StyleState(String identifier) { + this(identifier, null); + } + + StyleState(String identifier, @Nullable StyleState fallback) { + this.identifier = identifier; + this.fallback = fallback; + } + + public boolean inherits(StyleState state) { + StyleState 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/StyleValueType.java b/src/main/java/net/snackbag/vera/style/StyleValueType.java new file mode 100644 index 00000000..94de7127 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/StyleValueType.java @@ -0,0 +1,109 @@ +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.Easings; +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)), + + COLOR(VColor.black(), (from, to, easing, delta) -> from.ease(easing, to, delta)), + FONT(VFont.create(), (from, to, easing, delta) -> + VFont.create().withColor(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(Easings.LINEAR, (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( + from.get1().ease(easing, to.get1(), delta), + from.get2().ease(easing, to.get2(), delta), + from.get3().ease(easing, to.get3(), delta), + from.get4().ease(easing, to.get4(), delta) + )); + + 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("^[\\w-./]*:[\\w-./]*$")) return IDENTIFIER; + else if (bias == CURSOR && EnumUtils.getEnumIgnoreCase(VCursorShape.class, s) != null) return CURSOR; + else if (bias == EASING && Easings.getIgnoreCase(s) != null) return EASING; + 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) 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) return COLOR; + 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 Easings.getIgnoreCase(v); + } + + 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 (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..99147da5 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/VStyleSheet.java @@ -0,0 +1,261 @@ +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 T getKey(VWidget widget, String key) { + return getKey(widget, key, StyleState.DEFAULT); + } + + public T getKey(VWidget widget, String key, @Nullable StyleState state) { + if (state == null) state = StyleState.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); + + if (mixed.containsKey(key)) { + if (!mixed.get(key).containsKey(state)) 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 StyleState state) { + if (state == null) state = StyleState.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 StyleState state) { + if (state == null) state = StyleState.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 StyleState#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 StyleState 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, StyleState.DEFAULT); + } + + public void setKey(String clazz, String key, Object object) { + setKey(clazz, key, object, StyleState.DEFAULT); + } + + public void setKey(Class clazz, String key, Object object) { + setKey(clazz, key, object, StyleState.DEFAULT); + } + + public void setKey(VWidget widget, String key, Object value, @Nullable StyleState state) { + if (state == null) state = StyleState.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 StyleState state) { + if (state == null) state = StyleState.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 StyleState state) { + if (state == null) state = StyleState.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, StyleState.DEFAULT); + } + + public VColor.ColorModifier modifyKeyAsColor(VWidget widget, String key, @Nullable StyleState state) { + if (state == null) state = StyleState.DEFAULT; + + @Nullable StyleState 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, StyleState.DEFAULT); + } + + public VFont.FontModifier modifyKeyAsFont(VWidget widget, String key, @Nullable StyleState state) { + if (state == null) state = StyleState.DEFAULT; + + @Nullable StyleState 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, StyleState.DEFAULT); + } + + public VColor.ColorModifier modifyKeyAsFontColor(VWidget widget, String key, @Nullable StyleState state) { + if (state == null) state = StyleState.DEFAULT; + + // this is cursed + @Nullable StyleState 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/animation/AnimationEngine.java b/src/main/java/net/snackbag/vera/style/animation/AnimationEngine.java new file mode 100644 index 00000000..d50041ab --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/AnimationEngine.java @@ -0,0 +1,249 @@ +package net.snackbag.vera.style.animation; + +import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.event.Events; +import net.snackbag.vera.style.StyleState; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.animation.easing.VEasing; +import net.snackbag.vera.widget.VWidget; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Objects; + +/** + * Per-widget handler for animations. This is after stylesheet.getKey, so there is no differentiation between + * widget-specific styles, class styles and standard styles. + */ +public class AnimationEngine { + public final VWidget widget; + private final HashMap activeAnimations = new HashMap<>(); + private final HashMap unwindingAnimations = new HashMap<>(); + private final HashMap rewindingAnimations = new HashMap<>(); + + private long cacheId = 0; + private final HashMap cache = new HashMap<>(); + + public AnimationEngine(VWidget widget) { + this.widget = widget; + } + + public boolean isActive(String name) { + return activeAnimations.keySet().stream().anyMatch(anim -> anim.name.equals(name)); + } + + public boolean isUnwinding(String name) { + return unwindingAnimations.keySet().stream().anyMatch(anim -> anim.name.equals(name)); + } + + public boolean isRewinding(String name) { + return rewindingAnimations.keySet().stream().anyMatch(anim -> anim.name.equals(name)); + } + + /** + * Unwinds or kills animations whenever they come to their end. + * Called in {@link net.snackbag.mcvera.impl.MCVeraRenderer#renderApp(VeraApp)} + */ + public void update() { + final long time = System.currentTimeMillis(); + + HashMap animations = (HashMap) activeAnimations.clone(); + for (VAnimation animation : animations.keySet()) { + if ((time - getTimeSinceActive(animation) >= animation.getTotalTime() - animation.unwindTime) && animation.autoUnwindAtEnd) { + unwind(animation); + } + + if (time - getTimeSinceActive(animation) >= animation.getTotalTime() && animation.loopMode == LoopMode.NONE) { + widget.events.fire(Events.Animation.FINISH, animation, getTimeSinceActive(animation)); + + if (animation.writeFinalStateTarget != null) { + HashMap finalStyles = animation.getFinalStyles(); + for (String key : finalStyles.keySet()) { + widget.setStyle(key, animation.writeFinalStateTarget, finalStyles.get(key)); + } + } + + kill(animation); + } + } + } + + public HashMap getAffectedStyles(StyleState state) { + return widget.app.styleSheet.getResolvedKeys(widget, state); + } + + public void activate(VAnimation animation) { + activate(animation, false); + } + + public void activate(VAnimation animation, boolean override) { + if (!override && isActive(animation.name)) return; + activeAnimations.put(animation, System.currentTimeMillis()); + widget.events.fire(Events.Animation.BEGIN, animation); + } + + public void activateOrRewind(VAnimation animation) { + if (isUnwinding(animation.name)) rewind(animation); + else activate(animation); + } + + public void activateTransition(StyleState from, StyleState target, int time, VEasing easing) { + activate(createTransitionAnimation(from, target, time, easing)); + } + + public VAnimation createTransitionAnimation(StyleState from, StyleState target, int time, VEasing easing) { + VAnimation.Builder builder = new VAnimation.Builder(widget.app, VAnimation.INTERNAL_TRANSITION_ANIMATION_NAME); + + builder.unwindEasing(easing); + builder.unwindTime(time); + + HashMap fromStyles = getAffectedStyles(from); + HashMap targetStyles = getAffectedStyles(target); + fromStyles.forEach(targetStyles::putIfAbsent); + targetStyles.forEach(fromStyles::putIfAbsent); + + // beginning keyframe + builder.keyframe(0, frame -> { + for (String key : fromStyles.keySet()) { + frame.style(key, fromStyles.get(key)); + } + }, 1); + + // ending keyframe + builder.keyframe(time, frame -> { + for (String key : targetStyles.keySet()) { + frame.style(key, targetStyles.get(key)); + } + }, 1); + + return builder.build(); + } + + public void kill(VAnimation animation) { + Long begin = activeAnimations.remove(animation); + UnwindContext unwindCtx = unwindingAnimations.remove(animation); + RewindContext rewindCtx = rewindingAnimations.remove(animation); + + Long unwindBegun = unwindCtx != null ? unwindCtx.begun : null; + Long rewindBegun = rewindCtx != null ? rewindCtx.begun : null; + + Long nullableBegun = Vera.firstOf(Objects::nonNull, begin, unwindBegun, rewindBegun); + + if (nullableBegun != null) widget.events.fire(Events.Animation.FINISH, animation, nullableBegun); + } + + public void unwind(VAnimation animation) { + unwind(animation, false); + } + + public void unwind(VAnimation animation, boolean override) { + if (!isActive(animation.name)) return; + if (!override && isUnwinding(animation.name)) return; + + long time = System.currentTimeMillis(); + widget.events.fire(Events.Animation.UNWIND_BEGIN, animation); + + int rewindProgress = 0; + if (rewindingAnimations.containsKey(animation)) { + RewindContext rewindCtx = rewindingAnimations.remove(animation); + int previousUnwindProgress = rewindCtx.unwindProgress(); + int currentRewindTime = (int) (time - rewindCtx.begun()); + + rewindProgress = Math.max(0, previousUnwindProgress - currentRewindTime); + } + + unwindingAnimations.put(animation, new UnwindContext(time, rewindProgress)); + } + + public void rewind(VAnimation animation) { + rewind(animation, false); + } + + public void rewind(VAnimation animation, boolean override) { + if (!isActive(animation.name)) return; + if (!unwindingAnimations.containsKey(animation)) return; + if (!override && isRewinding(animation.name)) return; + + long time = System.currentTimeMillis(); + widget.events.fire(Events.Animation.REWIND_BEGIN, animation); + + UnwindContext unwindCtx = unwindingAnimations.remove(animation); + int totalUnwindProgress = unwindCtx.rewindProgress() + (int) (time - unwindCtx.begun()); + + rewindingAnimations.put(animation, new RewindContext(time, totalUnwindProgress)); + } + + public @Nullable VAnimation getIfEverActive(String name) { + return activeAnimations.keySet() + .stream() + .filter(anim -> anim.name.equals(name)) + .findFirst() + .orElse(null); + } + + public VAnimation[] getAllActive() { + return activeAnimations.keySet().toArray(new VAnimation[0]); + } + + public VAnimation[] getAllUnwinding() { + return unwindingAnimations.keySet().toArray(new VAnimation[0]); + } + + public VAnimation[] getAllRewinding() { + return rewindingAnimations.keySet().toArray(new VAnimation[0]); + } + + public void checkCache() { + if (cacheId != Vera.renderCacheId) { + cache.clear(); + cacheId = Vera.renderCacheId; + } + } + + public T animateStyle(String style, T value) { + checkCache(); + + if (value == null) return null; + if (!cache.containsKey(style)) { + StyleValueType type = StyleValueType.get(value, null); + cache.put(style, widget.app.pipeline.applyComposites(this, style, type, value)); + } + + return (T) cache.get(style); + } + + /** + * Gets the time an animation has been active since, if it isn't active at all it will return -1 + * + * @param animation the animation to check + * @return when the animation was started + */ + public long getTimeSinceActive(VAnimation animation) { + return activeAnimations.getOrDefault(animation, -1L); + } + + public @Nullable UnwindContext getUnwindContext(VAnimation animation) { + return unwindingAnimations.getOrDefault(animation, null); + } + + public @Nullable RewindContext getRewindContext(VAnimation animation) { + return rewindingAnimations.getOrDefault(animation, null); + } + + /** + * Context for animation rewinding + * + * @param begun the timestamp when the animation started rewinding + * @param unwindProgress the amount of milliseconds the animation has already been unwinding + */ + public record RewindContext(Long begun, int unwindProgress) {} + + /** + * Context for animation unwinding + * + * @param begun the timestamp when the animation started unwinding + * @param rewindProgress the amount of milliseconds the animation has already been rewinding + */ + public record UnwindContext(Long begun, int rewindProgress) {} +} diff --git a/src/main/java/net/snackbag/vera/style/animation/LoopMode.java b/src/main/java/net/snackbag/vera/style/animation/LoopMode.java new file mode 100644 index 00000000..0e998196 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/LoopMode.java @@ -0,0 +1,6 @@ +package net.snackbag.vera.style.animation; + +public enum LoopMode { + NONE, + REPEAT +} 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..b2e250e7 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/VAnimation.java @@ -0,0 +1,235 @@ +package net.snackbag.vera.style.animation; + +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.style.StyleState; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.animation.easing.Easings; +import net.snackbag.vera.style.animation.easing.VEasing; +import org.jetbrains.annotations.Nullable; +import oshi.util.tuples.Pair; + +import java.util.*; +import java.util.function.Consumer; + +/** + * Represents an animation template scoped to a specific {@link VeraApp} instance. + *

+ * Animations are app-specific and should not be shared across multiple apps. + * If you need to reuse an animation in different apps, define a method that + * generates a new instance of the animation for each target app. + */ +public class VAnimation { + public static final String INTERNAL_TRANSITION_ANIMATION_NAME = "vera-transition"; + + public final String name; + public final VeraApp app; + + public final int unwindTime; + public final boolean autoUnwindAtEnd; + public final VEasing unwindEasing; + public final LoopMode loopMode; + public final @Nullable StyleState writeFinalStateTarget; + + private final List keyframes = new ArrayList<>(); + protected final HashMap styleAffections = new HashMap<>(); + + private int totalTime = 0; + + public VAnimation( + String name, + int unwindTime, boolean autoUnwindAtEnd, VEasing unwindEasing, + LoopMode loopMode, @Nullable StyleState writeFinalStateTarget, + VeraApp app + ) { + this.name = name; + this.unwindTime = unwindTime; + this.autoUnwindAtEnd = autoUnwindAtEnd; + this.unwindEasing = unwindEasing; + this.totalTime = autoUnwindAtEnd ? unwindTime : 0; + this.loopMode = loopMode; + this.writeFinalStateTarget = writeFinalStateTarget; + + this.app = app; + } + + public void addKeyframe(VKeyframe keyframe) { + this.keyframes.add(keyframe); + keyframe.animation.set(this); + totalTime += keyframe.cumulatedTime; + } + + public boolean affects(String style) { + return styleAffections.containsKey(style); + } + + public @Nullable T calculateStyle(String style, T original, StyleValueType svt, long timeSinceActive, boolean addMonotoneEnd) { + // TODO: implement loop modes + + if (!affects(style)) return null; + + int margin = 0; + for (int i = 0; i < keyframes.size(); i++) { + VKeyframe frame = keyframes.get(i); + + if (timeSinceActive >= margin && timeSinceActive <= margin + frame.cumulatedTime) { + int timeInFrame = (int) timeSinceActive - margin; + boolean isTransition = timeInFrame <= frame.transitionTime; + + if (!isTransition) return (T) frame.styles.get(style).getB(); + + T before; + T after = getStyleForKeyframeDeep(i, style); + + if (i > 0) before = getStyleForKeyframeDeep(i - 1, style); + else before = original; + + float progress = timeInFrame / (float) frame.transitionTime; + + return (T) svt.animationTransition.apply(before, after, frame.easeIn, progress); + } + + margin += frame.cumulatedTime; + } + + if (addMonotoneEnd && timeSinceActive >= totalTime - unwindTime && timeSinceActive <= totalTime) { + return (T) keyframes.get(keyframes.size() - 1).styles.get(style).getB(); + } + + return original; + } + + public T getStyleForKeyframeDeep(int targetIndex, String style) { + VKeyframe target = keyframes.get(targetIndex); + + for (int i = targetIndex; !target.styles.containsKey(style); i--) { + target = keyframes.get(i - 1); + } + + Pair pair = target.styles.get(style); + return (T) StyleValueType.convert(pair.getB(), pair.getA()); + } + + public HashMap getFinalStyles() { + HashMap finalStyles = new HashMap<>(); + + if (keyframes.isEmpty()) return finalStyles; + for (String style : styleAffections.keySet()) { + finalStyles.put(style, getStyleForKeyframeDeep(keyframes.size() - 1, style)); + } + + return finalStyles; + } + + public int getTotalTime() { + return totalTime; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof VAnimation animation)) return false; + return Objects.equals(name, animation.name) && Objects.equals(app, animation.app); + } + + @Override + public int hashCode() { + return Objects.hash(name, app); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + builder.append(" (" + totalTime + "ms) {"); + builder.append("\n"); + + for (VKeyframe frame : keyframes) { + builder.append(" -> "); + builder.append(frame.transitionTime); + builder.append("ms -- "); + builder.append(frame.stayTime); + builder.append("ms: "); + builder.append(frame.easeIn); + builder.append("\n"); + + + for (String style : frame.styles.keySet()) { + Pair val = frame.styles.get(style); + + builder.append(" "); + builder.append(style); + builder.append(": "); + builder.append(val.getB()); + builder.append(" ("); + builder.append(val.getA()); + builder.append(")\n"); + } + } + + builder.append("}"); + + return builder.toString(); + } + + public VKeyframe[] getKeyframes() { + return keyframes.toArray(new VKeyframe[0]); + } + + public static class Builder { + private final String name; + private final VeraApp app; + + private LoopMode loopMode = LoopMode.NONE; + private @Nullable StyleState keepFinalStyle = null; + private int unwindTime = 0; + private boolean autoUnwindAtEnd = false; + private VEasing unwindEasing = Easings.LINEAR; + + private final List>> keyframes = new ArrayList<>(); + + public Builder(VeraApp app, String name) { + this.name = name; + this.app = app; + } + + public Builder loop(LoopMode mode) { + this.loopMode = mode; + return this; + } + + public Builder unwindTime(int ms) { + this.unwindTime = ms; + return this; + } + + public Builder unwindOnFinish() { + this.autoUnwindAtEnd = true; + return this; + } + + public Builder unwindEasing(VEasing easing) { + this.unwindEasing = easing; + return this; + } + + public Builder keepFinalStyle(StyleState writeTo) { + this.keepFinalStyle = writeTo; + return this; + } + + public Builder keyframe(int transitionMs, Consumer frame, int stayMs) { + keyframes.add(new Pair<>(new VKeyframe(transitionMs, stayMs), frame)); + return this; + } + + public VAnimation build() { + VAnimation animation = new VAnimation(name, unwindTime, autoUnwindAtEnd, unwindEasing, loopMode, keepFinalStyle, app); + + for (Pair> frame : keyframes) { + animation.addKeyframe(frame.getA()); + frame.getB().accept(frame.getA()); + } + + return animation; + } + } +} 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..8cc182ac --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/VKeyframe.java @@ -0,0 +1,40 @@ +package net.snackbag.vera.style.animation; + +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.animation.easing.Easings; +import net.snackbag.vera.style.animation.easing.VEasing; +import net.snackbag.vera.util.Once; +import org.jetbrains.annotations.NotNull; +import oshi.util.tuples.Pair; + +import java.util.HashMap; + +public class VKeyframe { + protected final Once animation = new Once<>(); + protected final int transitionTime; + protected final int stayTime; + protected final int cumulatedTime; + protected final HashMap> styles = new HashMap<>(); + + public @NotNull VEasing easeIn = Easings.LINEAR; + + public VKeyframe(int transitionTime, int stayTime) { + this.transitionTime = transitionTime; + this.stayTime = stayTime; + this.cumulatedTime = this.transitionTime + this.stayTime; + } + + public void style(String key, Object value) { + StyleValueType reservation = animation.get().app.styleSheet.getReservation(key); + if (reservation == null) throw new UnsupportedOperationException("Cannot set keyframe style to unreserved style key"); + + animation.get().styleAffections.merge(key, 1, Integer::sum); + + Object converted = StyleValueType.convert(value, reservation); + styles.put(key, new Pair<>(reservation, converted)); + } + + public boolean affects(String style) { + return styles.containsKey(style); + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/VeraPipeline.java b/src/main/java/net/snackbag/vera/style/animation/VeraPipeline.java new file mode 100644 index 00000000..c1e42e5c --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/VeraPipeline.java @@ -0,0 +1,54 @@ +package net.snackbag.vera.style.animation; + +import net.snackbag.vera.Vera; +import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.animation.composite.Composite; +import net.snackbag.vera.widget.VWidget; + +import java.util.ArrayList; +import java.util.List; + +public class VeraPipeline { + public final VeraApp app; + private final List passes = new ArrayList<>(); + private final List> cleanWidgets = new ArrayList<>(); + + public VeraPipeline(VeraApp app) { + this.app = app; + } + + public void addPass(Composite pass) { + setupPass(pass); + this.passes.add(pass); + } + + public void addPass(int index, Composite pass) { + setupPass(pass); + this.passes.add(index, pass); + } + + private void setupPass(Composite pass) { + boolean unique = pass.pipeline.setSafe(this); + if (!unique) throw new UnsupportedOperationException("Cannot setup composite pass if composite is already bound to pipeline"); + } + + public T applyComposites(AnimationEngine engine, String style, StyleValueType type, T in) { + Composite.Context ctx = new Composite.Context(engine,style, type, in); + boolean isNewFrame = false; + + for (Composite pass : passes) { + if (pass.frameTime != Vera.renderCacheId) { + pass.frameTime = Vera.renderCacheId; + cleanWidgets.clear(); + pass.generateUniforms(); + isNewFrame = true; + } + + if (!cleanWidgets.contains(engine.widget)) pass.applyWidget(engine.widget); + in = pass.applyStyle(ctx, in, isNewFrame); + } + + return in; + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/composite/AnimationComposite.java b/src/main/java/net/snackbag/vera/style/animation/composite/AnimationComposite.java new file mode 100644 index 00000000..b5d6a38b --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/composite/AnimationComposite.java @@ -0,0 +1,61 @@ +package net.snackbag.vera.style.animation.composite; + +import net.snackbag.vera.style.animation.AnimationEngine; +import net.snackbag.vera.style.animation.VAnimation; +import net.snackbag.vera.widget.VWidget; + +import java.util.HashMap; + +public class AnimationComposite extends Composite { + private final HashMap animations = new HashMap<>(); + private final HashMap animationTimes = new HashMap<>(); + private long time; // so we don't have to call it again + + @Override + public void generateUniforms() { + animations.clear(); + animationTimes.clear(); + time = System.currentTimeMillis(); + } + + @Override + public void applyWidget(VWidget widget) { + VAnimation[] active = widget.animations.getAllActive(); + + // Only store if we have active animations + if (active.length > 0) { + animations.put(widget.animations, active); + + for (VAnimation animation : active) { + animationTimes.put(animation, time - widget.animations.getTimeSinceActive(animation)); + } + } + } + + @Override + public T applyStyle(Context ctx, T in, boolean isNewFrame) { + // Most caching isn't necessary, since it's done earlier. + + AnimationEngine engine = ctx.engine(); + VAnimation[] engineAnimations = animations.get(engine); + + // early return + if (engineAnimations == null || engineAnimations.length == 0) return in; + + T out = in; + for (VAnimation animation : engineAnimations) { + // No need to check affects, since calculateStyle does this internally + T rv = animation.calculateStyle( + ctx.style(), + ctx.original(), + ctx.type(), + animationTimes.get(animation), + animation.autoUnwindAtEnd + ); + + if (rv != null) out = rv; + } + + return out; + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/composite/Composite.java b/src/main/java/net/snackbag/vera/style/animation/composite/Composite.java new file mode 100644 index 00000000..160ed1b4 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/composite/Composite.java @@ -0,0 +1,26 @@ +package net.snackbag.vera.style.animation.composite; + +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.animation.AnimationEngine; +import net.snackbag.vera.style.animation.VeraPipeline; +import net.snackbag.vera.util.Once; +import net.snackbag.vera.widget.VWidget; + +public abstract class Composite { + public Once pipeline = new Once<>(); + public long frameTime = 0; + + /** + * This method is called per-frame and is entirely independent of the given style. Hence, the name uniform. + */ + public void generateUniforms() {} + + public void applyWidget(VWidget widget) {} + + public T applyStyle(Context ctx, T in, boolean isNewFrame) { + return in; + } + + public record Context(AnimationEngine engine, String style, StyleValueType type, T original) { + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/composite/WindingComposite.java b/src/main/java/net/snackbag/vera/style/animation/composite/WindingComposite.java new file mode 100644 index 00000000..ee9159ad --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/composite/WindingComposite.java @@ -0,0 +1,70 @@ +package net.snackbag.vera.style.animation.composite; + +import net.snackbag.vera.style.animation.AnimationEngine; +import net.snackbag.vera.style.animation.VAnimation; +import net.snackbag.vera.widget.VWidget; + +import java.util.HashMap; + +public class WindingComposite extends Composite { + private final HashMap unwindingAnimations = new HashMap<>(); + private final HashMap rewindingAnimations = new HashMap<>(); + private long time; + + @Override + public void generateUniforms() { + time = System.currentTimeMillis(); + unwindingAnimations.clear(); + rewindingAnimations.clear(); + } + + @Override + public void applyWidget(VWidget widget) { + unwindingAnimations.put(widget.animations, widget.animations.getAllUnwinding()); + rewindingAnimations.put(widget.animations, widget.animations.getAllRewinding()); + } + + @Override + public T applyStyle(Context ctx, T in, boolean isNewFrame) { + AnimationEngine engine = ctx.engine(); + VAnimation[] unwinding = unwindingAnimations.get(engine); + VAnimation[] rewinding = rewindingAnimations.get(engine); + + T out = in; + T original = ctx.original(); + String style = ctx.style(); + + for (VAnimation animation : unwinding) { + if (!animation.affects(style)) continue; + if (animation.unwindTime <= 0) { + out = original; + continue; + } + + AnimationEngine.UnwindContext unwindCtx = engine.getUnwindContext(animation); + int totalProgress = unwindCtx.rewindProgress() + (int) (time - unwindCtx.begun()); + float delta = Math.min((float) totalProgress / (float) animation.unwindTime, 1f); + + out = (T) ctx.type().animationTransition.apply(in, original, animation.unwindEasing, delta); + } + + for (VAnimation animation : rewinding) { + if (!animation.affects(style)) continue; + if (animation.unwindTime <= 0) { + out = in; + continue; + } + + AnimationEngine.RewindContext rewindCtx = engine.getRewindContext(animation); + int currentRewindTime = (int) (time - rewindCtx.begun()); + + // calculate reverse: start from how much we had unwound, go back towards 0 + int remainingUnwindProgress = Math.max(0, rewindCtx.unwindProgress() - currentRewindTime); + float delta = Math.min((float) remainingUnwindProgress / (float) animation.unwindTime, 1f); + + out = (T) ctx.type().animationTransition.apply(in, original, animation.unwindEasing, delta); + } + + return out; + } +} diff --git a/src/main/java/net/snackbag/vera/style/animation/easing/Easings.java b/src/main/java/net/snackbag/vera/style/animation/easing/Easings.java new file mode 100644 index 00000000..ca2c26dd --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/easing/Easings.java @@ -0,0 +1,11 @@ +package net.snackbag.vera.style.animation.easing; + +import net.snackbag.vera.Vera; + +public class Easings { + public static final VLinearEasing LINEAR = new VLinearEasing(); + + public static VEasing getIgnoreCase(String name) { + return Vera.registrar.getEasingIgnoreCase(name); + } +} 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..3e356ba7 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/easing/VEasing.java @@ -0,0 +1,13 @@ +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 abstract int apply(int from, int to, float delta); +} diff --git a/src/main/java/net/snackbag/vera/style/animation/easing/VLinearEasing.java b/src/main/java/net/snackbag/vera/style/animation/easing/VLinearEasing.java new file mode 100644 index 00000000..3d71e655 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/animation/easing/VLinearEasing.java @@ -0,0 +1,18 @@ +package net.snackbag.vera.style.animation.easing; + +public class VLinearEasing extends VEasing { + protected VLinearEasing() { + super("linear"); + } + + @Override + public float apply(float from, float to, float delta) { + return from + delta * (to - from); + } + + @Override + public int apply(int from, int to, float delta) { + final float fromF = (float) from; + return Math.round(fromF + delta * ((float) to - fromF)); + } +} 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..f9daabad --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/CheckBoxStandardStyle.java @@ -0,0 +1,20 @@ +package net.snackbag.vera.style.standard; + +import net.snackbag.vera.core.VCursorShape; +import net.snackbag.vera.style.StyleState; +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, StyleState.HOVERED); + } + + @Override + public void reserve(VStyleSheet sheet) { + sheet.reserveType("src", StyleValueType.IDENTIFIER); + sheet.reserveType("src-checked", StyleValueType.IDENTIFIER); + } +} 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..8750e490 --- /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.StyleState; +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, StyleState.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/ImageStandardStyle.java b/src/main/java/net/snackbag/vera/style/standard/ImageStandardStyle.java new file mode 100644 index 00000000..4c9d2839 --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/ImageStandardStyle.java @@ -0,0 +1,15 @@ +package net.snackbag.vera.style.standard; + +import net.snackbag.vera.style.StyleValueType; +import net.snackbag.vera.style.VStyleSheet; + +public class ImageStandardStyle implements VStandardStyle { + @Override + public void apply(VStyleSheet sheet) { + } + + @Override + public void reserve(VStyleSheet sheet) { + sheet.reserveType("src", StyleValueType.IDENTIFIER); + } +} 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..03d87ca0 --- /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-color", 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-color", StyleValueType.COLOR); + 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..ca4fba13 --- /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.StyleState; +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-color", VColor.of(0, 120, 215, 0.2f)); + sheet.setKey(VLineInput.class, "background-color", VColor.transparent()); + sheet.setKey(VLineInput.class, "cursor", VCursorShape.TEXT, StyleState.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-color", StyleValueType.COLOR); + sheet.reserveType("background-color", StyleValueType.COLOR); + 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..b3c683a5 --- /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-color", VColor.black()); + } + + @Override + public void reserve(VStyleSheet sheet) { + sheet.reserveType("background-color", StyleValueType.COLOR); + } +} 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..75034653 --- /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.StyleState; +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, StyleState.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..36bc6a5a --- /dev/null +++ b/src/main/java/net/snackbag/vera/style/standard/WidgetStandardStyle.java @@ -0,0 +1,39 @@ +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.Easings; +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", Easings.LINEAR); + } + + @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); + + // 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..c9be1f68 --- /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.Events; +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(Events.Widget.DRAG_LEFT_CLICK, createContext()); + case MIDDLE -> target.events.fire(Events.Widget.DRAG_MIDDLE_CLICK, createContext()); + case RIGHT -> target.events.fire(Events.Widget.DRAG_RIGHT_CLICK, createContext()); + } + } +} diff --git a/src/main/java/net/snackbag/vera/util/Geometry.java b/src/main/java/net/snackbag/vera/util/Geometry.java new file mode 100644 index 00000000..3e66b01c --- /dev/null +++ b/src/main/java/net/snackbag/vera/util/Geometry.java @@ -0,0 +1,20 @@ +package net.snackbag.vera.util; + +public class Geometry { + /** + * 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/Once.java b/src/main/java/net/snackbag/vera/util/Once.java new file mode 100644 index 00000000..4a6ac64c --- /dev/null +++ b/src/main/java/net/snackbag/vera/util/Once.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 Once { + @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 Once 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..6557c610 100644 --- a/src/main/java/net/snackbag/vera/widget/VCheckBox.java +++ b/src/main/java/net/snackbag/vera/widget/VCheckBox.java @@ -3,128 +3,48 @@ 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.event.Events; import net.snackbag.vera.event.VCheckedStateChange; -import org.jetbrains.annotations.Nullable; +import net.snackbag.vera.style.StyleState; 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); + this( + app, + new Identifier(MinecraftVera.MOD_ID, "widgets/checkmark/default.png"), + new Identifier(MinecraftVera.MOD_ID, "widgets/checkmark/checked.png") + ); } public VCheckBox(VeraApp app, Identifier defaultTexture, Identifier checkedTexture) { - this(app); - - this.defaultTexture = defaultTexture; - this.checkedTexture = checkedTexture; + this(app, defaultTexture, checkedTexture, 15, 15); } 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); + super(0, 0, width, height, app); - this.defaultHoverTexture = defaultHoverTexture; - this.checkedHoverTexture = checkedHoverTexture; - } - - public VCheckBox( - VeraApp app, - Identifier defaultTexture, @Nullable Identifier defaultHoverTexture, - Identifier checkedTexture, @Nullable Identifier checkedHoverTexture, - int width, int height) { - this(app, defaultTexture, defaultHoverTexture, checkedTexture, checkedHoverTexture); + this.checked = false; - setSize(width, height); + setStyle("src", defaultTexture); + setStyle("src-checked", checkedTexture); } @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); + StyleState state = createStyleState(); + Identifier texture = checked ? getStyle("src-checked", state) : getStyle("src", state); + + Vera.renderer.drawImage(app, getX(), getY(), width, height, 0, texture); } @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(Events.Widget.LEFT_CLICK)) setChecked(!checked); } public boolean isChecked() { @@ -134,10 +54,10 @@ public boolean isChecked() { public void setChecked(boolean checked) { this.checked = checked; - fireEvent("vcheckbox-checked", checked); + events.fire(Events.CheckBox.CHECK_STATE_CHANGED, checked); } public void onCheckStateChange(VCheckedStateChange runnable) { - registerEventExecutor("vcheckbox-checked", args -> runnable.run((boolean) args[0])); + events.register(Events.CheckBox.CHECK_STATE_CHANGED, args -> runnable.run((boolean) args[0])); } } diff --git a/src/main/java/net/snackbag/vera/widget/VDropdown.java b/src/main/java/net/snackbag/vera/widget/VDropdown.java index 8ce4292f..e0940611 100644 --- a/src/main/java/net/snackbag/vera/widget/VDropdown.java +++ b/src/main/java/net/snackbag/vera/widget/VDropdown.java @@ -3,20 +3,24 @@ 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.Events; import net.snackbag.vera.event.VItemSwitchEvent; -import net.snackbag.vera.modifier.VPaddingWidget; +import net.snackbag.vera.modifier.VHasFont; +import net.snackbag.vera.style.StyleState; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; -public class VDropdown extends VWidget implements VPaddingWidget { +// 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; @@ -26,18 +30,22 @@ public VDropdown(VeraApp 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() { + StyleState state = createStyleState(); + + VColor backgroundColor = getStyle("background-color", state); + VFont font = getStyle("font", state); + + int x = getX(); + int y = getY(); + Vera.renderer.drawRect( - app, getHitboxX(), getHitboxY(), getHitboxWidth(), getHitboxHeight(), + app, getEffectiveX(), getEffectiveY(), getEffectiveWidth(), getEffectiveHeight(), 0, backgroundColor ); @@ -52,9 +60,9 @@ app, getHitboxX(), getHitboxY(), getHitboxWidth(), getHitboxHeight(), if (isHovered) { Vera.renderer.drawRect( app, - getHitboxX(), + getEffectiveX(), y + (i * (itemSpacing + font.getSize() / 2)), - getHitboxWidth(), + getEffectiveWidth(), font.getSize() / 2 + itemSpacing, 0, itemHoverColor ); @@ -69,7 +77,7 @@ app, getHitboxX(), getHitboxY(), getHitboxWidth(), getHitboxHeight(), ); } - Vera.renderer.drawText(app, textX, textY, 0, item.name, isHovered ? hoverFont : font); + Vera.renderer.drawText(app, textX, textY, 0, item.name, isHovered ? itemHoverFont : font); } } else { Vera.renderer.drawText(app, x, y, 0, 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(Events.Dropdown.SELECTOR_OPEN); + else events.fire(Events.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() { + StyleState 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 Events.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 Events.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 Events.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 Events.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 Events.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(Events.Dropdown.ITEM_SWITCH, args -> runnable.run((int) args[0])); } public void onSelectorOpen(Runnable runnable) { - registerEventExecutor("vdropdown-selector-open", runnable); + events.register(Events.Dropdown.SELECTOR_OPEN, runnable); } public void onSelectorClose(Runnable runnable) { - registerEventExecutor("vdropdown-selector-close", runnable); + events.register(Events.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(Events.Dropdown.ITEM_SWITCH, selectedItem); } public void addItem(String name) { @@ -317,6 +281,11 @@ public List getItems() { return items; } + @Override + public VeraApp getApp() { + return app; + } + public static class Item { private String name; private final @Nullable Runnable leftClick; diff --git a/src/main/java/net/snackbag/vera/widget/VImage.java b/src/main/java/net/snackbag/vera/widget/VImage.java index 59a2ce23..0be47e49 100644 --- a/src/main/java/net/snackbag/vera/widget/VImage.java +++ b/src/main/java/net/snackbag/vera/widget/VImage.java @@ -3,32 +3,25 @@ import net.minecraft.util.Identifier; import net.snackbag.vera.Vera; import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.style.StyleState; public class VImage extends VWidget { - private Identifier path; - - public VImage(Identifier path, int width, int height, VeraApp app) { + public VImage(Identifier src, int width, int height, VeraApp app) { super(0, 0, width, height, app); - this.path = path; + setStyle("src", src); 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)); + public VImage(String src, int width, int height, VeraApp app) { + this(new Identifier(src), width, height, app); } @Override public void render() { - VeraApp app = getApp(); - Vera.renderer.drawImage(app, x, y, width, height, rotation, path); + StyleState state = createStyleState(); + Identifier src = getStyle("src", state); + + Vera.renderer.drawImage(app, getX(), getY(), width, height, rotation, src); } } diff --git a/src/main/java/net/snackbag/vera/widget/VLabel.java b/src/main/java/net/snackbag/vera/widget/VLabel.java index e3680dea..937a5e80 100644 --- a/src/main/java/net/snackbag/vera/widget/VLabel.java +++ b/src/main/java/net/snackbag/vera/widget/VLabel.java @@ -2,48 +2,31 @@ 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.StyleState; -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, VeraApp 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; - } - - public String getText() { - return text; + alignment = VHAlignmentFlag.LEFT; } - public VFont getFont() { - return font; - } - - public void setFont(VFont font) { - this.font = font; - } - - public VColor getBackgroundColor() { - return backgroundColor; - } + public VLabel(String text, VeraApp app) { + this(text, 0, 0, 100, 16, app); - public void setBackgroundColor(VColor backgroundColor) { - this.backgroundColor = backgroundColor; + adjustSize(); } - public VColor.ColorModifier modifyBackgroundColor() { - return new VColor.ColorModifier(backgroundColor, this::setBackgroundColor); + public String getText() { + return text; } public void setText(String text) { @@ -51,78 +34,66 @@ 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); } + @Override + public VeraApp getApp() { + return app; + } + + public VColor.ColorModifier modifyColor(String key) { + return app.styleSheet.modifyKeyAsColor(this, key); + } + @Override public void render() { - VeraApp app = getApp(); + StyleState state = createStyleState(); + + VFont font = getStyle("font", state); + VColor backgroundColor = getStyle("background-color", state); + V4Int padding = getStyle("padding", state); Vera.renderer.drawRect( app, - getHitboxX(), - getHitboxY(), - getHitboxWidth(), - getHitboxHeight(), + getX(), + getY(), + getEffectiveWidth(), + getEffectiveHeight(), rotation, backgroundColor ); + int usualX = getX() + padding.get3(); + int usualY = getY() + 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(app, usualX, usualY, rotation, text, font); + case CENTER -> Vera.renderer.drawText(app, getX() + getWidth() / 2 - Vera.provider.getTextWidth(text, font) / 2, usualY, rotation, text, font); + case RIGHT -> Vera.renderer.drawText(app, getX() + getEffectiveWidth() - padding.get4() - Vera.provider.getTextWidth(text, font), usualY, rotation, 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..76cebb81 100644 --- a/src/main/java/net/snackbag/vera/widget/VLineInput.java +++ b/src/main/java/net/snackbag/vera/widget/VLineInput.java @@ -4,54 +4,52 @@ import net.minecraft.client.util.InputUtil; import net.snackbag.vera.Vera; import net.snackbag.vera.core.*; +import net.snackbag.vera.core.v4.V4Int; +import net.snackbag.vera.event.Events; 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.StyleState; 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 VColor backgroundColor; - private V4Int padding; - public VLineInput(VeraApp 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); } @Override public void render() { + StyleState state = createStyleState(); + + VFont font = getStyle("font", state); + VFont placeholderFont = getStyle("placeholder-font", state); + VColor backgroundColor = getStyle("background-color", state); + VColor textSelectionColor = getStyle("select-color", state); + + int x = getX(); + int y = getY(); + Vera.renderer.drawRect( app, - getHitboxX() + app.getX(), - getHitboxY() + app.getY(), - getHitboxWidth(), - getHitboxHeight(), + getEffectiveX() + app.getX(), + getEffectiveY() + app.getY(), + getEffectiveWidth(), + getEffectiveHeight(), rotation, backgroundColor ); @@ -93,58 +91,24 @@ public void render() { @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); - } + StyleState state = createStyleState(); + VFont font = getStyle("font", state); - 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); - } + int x = getX(); - public VColor.ColorModifier modifyPlaceholderFontColor() { - return new VColor.ColorModifier(font.getColor(), (color) -> setFont(font.withColor(color))); - } - - public VColor getBackgroundColor() { - return backgroundColor; - } + if (event.equals(Events.Widget.LEFT_CLICK)) { + textSelection.clear(); - public void setBackgroundColor(VColor backgroundColor) { - this.backgroundColor = backgroundColor; + if (Vera.getMouseX() < x) cursorPos = 0; + else if (Vera.getMouseX() > x + Vera.provider.getTextWidth(text, font)) cursorPos = text.length(); + } } - public VColor.ColorModifier modifyBackgroundColor() { - return new VColor.ColorModifier(backgroundColor, this::setBackgroundColor); + @Override + public VeraApp getApp() { + return app; } public String getText() { @@ -153,7 +117,7 @@ public String getText() { public void setText(String text) { this.text = text; - fireEvent("vline-change"); + events.fire(Events.LineInput.CHANGE); } public boolean isSelectingText() { @@ -177,18 +141,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 +162,23 @@ public String getPlaceholderText() { } public void onLineChanged(Runnable runnable) { - registerEventExecutor("vline-change", runnable); + events.register(Events.LineInput.CHANGE, runnable); } public void onCursorMove(Runnable runnable) { - registerEventExecutor("vline-cursor-move", runnable); + events.register(Events.LineInput.CURSOR_MOVE, runnable); } public void onCursorMoveLeft(Runnable runnable) { - registerEventExecutor("vline-cursor-move-left", runnable); + events.register(Events.LineInput.CURSOR_MOVE_LEFT, runnable); } public void onCursorMoveRight(Runnable runnable) { - registerEventExecutor("vline-cursor-move-right", runnable); + events.register(Events.LineInput.CURSOR_MOVE_RIGHT, runnable); } public void onAddCharLimited(VCharLimitedEvent runnable) { - registerEventExecutor("vline-add-char-limited", args -> runnable.run((char) args[0])); + events.register(Events.LineInput.ADD_CHAR_LIMITED, args -> runnable.run((char) args[0])); } @Override @@ -311,32 +263,32 @@ 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"); + events.fire(Events.LineInput.CURSOR_MOVE); + events.fire(Events.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"); + events.fire(Events.LineInput.CURSOR_MOVE); + events.fire(Events.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"); + events.fire(Events.LineInput.CURSOR_MOVE); + events.fire(Events.LineInput.CURSOR_MOVE_LEFT); } else if (isDown(GLFW.GLFW_KEY_RIGHT) && isCtrlDown()) { cursorPos = text.length(); - fireEvent("vline-cursor-move"); - fireEvent("vline-cursor-move-right"); + events.fire(Events.LineInput.CURSOR_MOVE); + events.fire(Events.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"); + events.fire(Events.LineInput.CURSOR_MOVE); + events.fire(Events.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"); + events.fire(Events.LineInput.CURSOR_MOVE); + events.fire(Events.LineInput.CURSOR_MOVE_RIGHT); } super.keyPressed(keyCode, scanCode, modifiers); @@ -368,12 +320,12 @@ private void handleSelectionKeyPress(int keyCode) { cursorPos = newPos; textSelection.endPos = newPos; - fireEvent("vline-cursor-move"); + events.fire(Events.LineInput.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(Events.LineInput.ADD_CHAR_LIMITED, insertion.charAt(0)); return; } @@ -381,7 +333,7 @@ private void insertText(String insertion) { String back = text.substring(cursorPos); text = front + insertion + back; cursorPos += insertion.length(); - fireEvent("vline-change"); + events.fire(Events.LineInput.CHANGE); } private void deleteSelectedText() { @@ -395,7 +347,7 @@ private void deleteSelectedText() { text = front + back; cursorPos = start; clearTextSelection(); - fireEvent("vline-change"); + events.fire(Events.LineInput.CHANGE); } private void replaceSelectedText(String replacement) { @@ -405,7 +357,7 @@ 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(Events.LineInput.ADD_CHAR_LIMITED, replacement.charAt(0)); return; } @@ -414,7 +366,7 @@ private void replaceSelectedText(String replacement) { text = front + replacement + back; cursorPos = start + replacement.length(); clearTextSelection(); - fireEvent("vline-change"); + events.fire(Events.LineInput.CHANGE); } @@ -433,47 +385,46 @@ public void setCursorPos(int cursorPos) { this.cursorPos = cursorPos; } - public @Nullable VColor getCursorColor() { - return cursorColor; - } - public VColor getCursorColorSafe() { - return cursorColor == null ? font.getColor() : cursorColor; - } + StyleState state = createStyleState(); - public void setCursorColor(@Nullable VColor cursorColor) { - this.cursorColor = cursorColor; - } + VColor style = getStyleOrDefault("cursor-color", null, state); + VFont font = getStyle("font", state); - @Override - public V4Int getPadding() { - return padding; + return style == null ? font.getColor() : style; } @Override - public void setPadding(V4Int padding) { - this.padding = padding; - } + public int getEffectiveWidth() { + StyleState state = createStyleState(); + + VFont font = getStyle("font", state); + V4Int padding = getStyle("padding", state); - @Override - public int getHitboxWidth() { return Math.max(width, Vera.provider.getTextWidth(text, font)) + padding.get3() + padding.get4(); } @Override - public int getHitboxHeight() { + public int getEffectiveHeight() { + StyleState state = createStyleState(); + + VFont font = getStyle("font", state); + V4Int padding = getStyle("padding", state); + return Vera.provider.getTextHeight(text, font) + padding.get1() + padding.get2(); } @Override - public int getHitboxX() { - return x - padding.get4(); + public int getEffectiveX() { + V4Int padding = getStyle("padding", createStyleState()); + return getX() - padding.get4(); } @Override - public int getHitboxY() { - return y - padding.get1(); + public int getEffectiveY() { + V4Int padding = getStyle("padding", createStyleState()); + return getY() - padding.get1(); } @Override @@ -485,7 +436,7 @@ 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(Events.LineInput.ADD_CHAR_LIMITED, chr); return; } @@ -495,11 +446,11 @@ public void charTyped(char chr, int modifiers) { text = front + chr + back; cursorPos = start + 1; clearTextSelection(); - fireEvent("vline-change"); + events.fire(Events.LineInput.CHANGE); } else { // Normal character insertion if (maxChars > -1 && text.length() >= maxChars) { - fireEvent("vline-add-char-limited", chr); + events.fire(Events.LineInput.ADD_CHAR_LIMITED, chr); return; } @@ -508,7 +459,7 @@ public void charTyped(char chr, int modifiers) { text = front + chr + back; cursorPos += 1; - fireEvent("vline-change"); + events.fire(Events.LineInput.CHANGE); } } super.charTyped(chr, modifiers); @@ -587,7 +538,7 @@ private void deleteText(int start, int end) { builder.delete(start, end); text = builder.toString(); cursorPos = Math.min(start, text.length()); - fireEvent("vline-change"); + events.fire(Events.LineInput.CHANGE); } 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..7d284b0a 100644 --- a/src/main/java/net/snackbag/vera/widget/VRect.java +++ b/src/main/java/net/snackbag/vera/widget/VRect.java @@ -3,31 +3,19 @@ import net.snackbag.vera.Vera; import net.snackbag.vera.core.VColor; import net.snackbag.vera.core.VeraApp; +import net.snackbag.vera.style.StyleState; 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 VColor getColor() { - return color; - } - - public void setColor(VColor color) { - this.color = color; - } - - public VColor.ColorModifier modifyColor() { - return new VColor.ColorModifier(color, this::setColor); - } - @Override public void render() { - Vera.renderer.drawRect(app, x, y, width, height, rotation, color); + StyleState state = createStyleState(); + + Vera.renderer.drawRect(app, getX(), getY(), width, height, rotation, getStyle("background-color", 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..62a78f89 100644 --- a/src/main/java/net/snackbag/vera/widget/VTabWidget.java +++ b/src/main/java/net/snackbag/vera/widget/VTabWidget.java @@ -1,42 +1,42 @@ 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.event.Events; +import net.snackbag.vera.modifier.VHasFont; +import net.snackbag.vera.style.StyleState; 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(VeraApp 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() { + StyleState 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; + int x = getX(); + int y = getY(); + for (String key : tabs.keySet()) { int textWidth = Vera.provider.getTextWidth(key, font); @@ -46,7 +46,7 @@ public void render() { Vera.renderer.drawRect(app, x + marginX - itemSpacingLeft, y, itemSpacingLeft + itemSpacingRight + textWidth, - getHitboxHeight(), 0, + getEffectiveHeight(), 0, activeTab != null && activeTab == i ? selectedBackgroundColor: defaultBackgroundColor ); @@ -58,42 +58,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 Events.Widget.MOUSE_MOVE -> getHoveredTabIndex((int) args[0]); - case "hover-enter" -> getHoveredTabIndex(Vera.getMouseX()); - case "hover-leave" -> hoveredTab = null; + case Events.Widget.HOVER -> getHoveredTabIndex(Vera.getMouseX()); + case Events.Widget.HOVER_LEAVE -> hoveredTab = null; - case "left-click" -> { + case Events.Widget.LEFT_CLICK -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-left-click", hoveredTab); + events.fire(Events.TabWidget.TAB_LEFT_CLICK, hoveredTab); setActiveTab(hoveredTab); } - case "left-click-release" -> { + case Events.Widget.LEFT_CLICK_RELEASE -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-left-click-release", hoveredTab); + events.fire(Events.TabWidget.TAB_LEFT_CLICK_RELEASE, hoveredTab); } - case "middle-click" -> { + case Events.Widget.MIDDLE_CLICK -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-middle-click", hoveredTab); + events.fire(Events.TabWidget.TAB_MIDDLE_CLICK, hoveredTab); } - case "middle-click-release" -> { + case Events.Widget.MIDDLE_CLICK_RELEASE -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-middle-click-release", hoveredTab); + events.fire(Events.TabWidget.TAB_MIDDLE_CLICK_RELEASE, hoveredTab); } - case "right-click" -> { + case Events.Widget.RIGHT_CLICK -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-right-click", hoveredTab); + events.fire(Events.TabWidget.TAB_RIGHT_CLICK, hoveredTab); } - case "right-click-release" -> { + case Events.Widget.RIGHT_CLICK_RELEASE -> { if (!isValidTabIndex(hoveredTab)) return; - fireEvent("vtabwidget-tab-right-click-release", hoveredTab); + events.fire(Events.TabWidget.TAB_RIGHT_CLICK_RELEASE, hoveredTab); } } - - super.handleBuiltinEvent(event, args); } public boolean isValidTabIndex(@Nullable Integer index) { @@ -106,7 +106,13 @@ public boolean isValidTabIndex(@Nullable Integer index) { } public int getHoveredTabIndex(int mouseX) { - int relativeX = mouseX - getHitboxX(); + StyleState state = createStyleState(); + + VFont font = getStyle("font", state); + int itemSpacingLeft = getStyle("item-spacing-left", state); + int itemSpacingRight = getStyle("item-spacing-right", state); + + int relativeX = mouseX - getEffectiveX(); int currentX = 0; int index = 0; @@ -116,7 +122,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(Events.TabWidget.TAB_HOVER_CHANGE, hoveredTab); } hoveredTab = index; @@ -135,31 +141,31 @@ public int getHoveredTabIndex(int mouseX) { } public void onTabHoverChange(Consumer runnable) { - registerEventExecutor("vtabwidget-tab-hover-change", (args) -> runnable.accept((int) args[0])); + events.register(Events.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(Events.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(Events.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(Events.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(Events.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(Events.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(Events.TabWidget.TAB_RIGHT_CLICK_RELEASE, (args) -> runnable.accept((int) args[0])); } public void addTab(String tab, VWidget... widgets) { @@ -192,12 +198,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() { + StyleState 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 +234,16 @@ public void setActiveTab(@Nullable Integer activeTab) { this.activeTab = activeTab; } - public VFont getFont() { - return font; + public VColor.ColorModifier modifyBackgroundColorSelected() { + return app.styleSheet.modifyKeyAsColor(this, "background-color-selected"); } - public void setFont(VFont font) { - this.font = font; + public VColor.ColorModifier modifyBackgroundColor() { + return app.styleSheet.modifyKeyAsColor(this, "background-color"); } - 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 void setItemSpacingRight(int itemSpacingRight) { - this.itemSpacingRight = itemSpacingRight; + @Override + public VeraApp getApp() { + return app; } } diff --git a/src/main/java/net/snackbag/vera/widget/VWidget.java b/src/main/java/net/snackbag/vera/widget/VWidget.java index d31bb957..2bc8d70f 100644 --- a/src/main/java/net/snackbag/vera/widget/VWidget.java +++ b/src/main/java/net/snackbag/vera/widget/VWidget.java @@ -1,184 +1,142 @@ 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.StyleState; +import net.snackbag.vera.style.animation.AnimationEngine; +import net.snackbag.vera.style.animation.VAnimation; +import net.snackbag.vera.util.DragHandler; 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; +import java.util.*; + +public abstract class VWidget> extends VElement { 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; + 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 StyleState prevStyleState = StyleState.DEFAULT; - private final HashMap> eventExecutors; - private final List> visibilityConditions; + public final AnimationEngine animations = new AnimationEngine(this); + public final LinkedHashSet classes = new LinkedHashSet<>(); 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); + super(app, x, y, width, height); - addVisibilityCondition(this::isVisible); + this.rotation = 0; } public abstract void render(); - public int getX() { - return x; - } - - public int getY() { - return y; - } - public int getHitboxX() { - return getX(); + return getEffectiveX(); } public int getHitboxY() { - return getY(); - } - - public int getWidth() { - return width; + return getEffectiveY(); } public int getHitboxWidth() { - return getWidth(); - } - - public int getHeight() { - return height; + return getEffectiveWidth(); } public int getHitboxHeight() { - return getHeight(); + return getEffectiveHeight(); } - public void setWidth(int width) { - this.width = width; + @SuppressWarnings("unchecked") + public void setStyle(String key, V... value) { + app.styleSheet.setKey(this, key, value); } - public void setHeight(int height) { - this.height = height; + @SuppressWarnings("unchecked") + public void setStyle(String key, StyleState state, V... value) { + app.styleSheet.setKey(this, key, value, state); } - public V4Color getBorder() { - return border; + public V getStyle(String key) { + return animations.animateStyle(key, app.styleSheet.getKey(this, key)); } - public void setBorder(V4Color border) { - this.border = border; + public V getStyle(String key, StyleState state) { + return animations.animateStyle(key, app.styleSheet.getKey(this, key, state)); } - public void setBorder(VColor all) { - setBorder(new V4Color(all)); + public V getStyleOrDefault(String key, V dflt) { + V style = getStyle(key); + return style != null ? style : dflt; } - public void setBorder(VColor tb, VColor lr) { - setBorder(new V4Color(tb, lr)); + public V getStyleOrDefault(String key, V dflt, StyleState state) { + V style = getStyle(key, state); + return style != null ? style : dflt; } - public void setBorder(VColor top, VColor bottom, VColor left, VColor right) { - setBorder(new V4Color(top, bottom, left, right)); - } + public StyleState createStyleState() { + // Clicks first + if (leftClickDown) return StyleState.LEFT_CLICKED; + else if (middleClickDown) return StyleState.MIDDLE_CLICKED; + else if (rightClickDown) return StyleState.RIGHT_CLICKED; - public V4Int getBorderSize() { - return borderSize; - } + else if (DragHandler.isDragging() && DragHandler.target == this) { + return switch (DragHandler.button) { + case LEFT -> StyleState.LC_DRAGGING; + case MIDDLE -> StyleState.MC_DRAGGING; + case RIGHT -> StyleState.RC_DRAGGING; + }; + } - public void setBorderSize(V4Int borderSize) { - this.borderSize = borderSize; + // Hover as last, since everything else is hover too + else if (isHovered()) return StyleState.HOVERED; + else return StyleState.DEFAULT; } - public void setBorderSize(int all) { - setBorderSize(new V4Int(all)); - } + public void renderBorder() { + // TODO: [Render Rework] Better border rendering - public void setBorderSize(int tb, int lr) { - setBorderSize(new V4Int(tb, lr)); - } + StyleState state = createStyleState(); - public void setBorderSize(int top, int bottom, int left, int right) { - setBorderSize(new V4Int(top, bottom, left, right)); - } + V4Color borderColor = getStyle("border-color", state); + V4Int borderSize = getStyle("border-size", state); - public void renderBorder() { // Top - Vera.renderer.drawRect(app, getHitboxX(), getHitboxY() - borderSize.get1(), getHitboxWidth(), borderSize.get1(), 0, border.get1()); + Vera.renderer.drawRect(app, getEffectiveX(), getEffectiveY() - borderSize.get1(), getEffectiveWidth(), borderSize.get1(), 0, 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(app, getEffectiveX() - borderSize.get3(), getEffectiveY() - borderSize.get1(), borderSize.get3(), borderSize.get1(), 0, borderColor.get1()); } // Bottom - Vera.renderer.drawRect(app, getHitboxX(), getHitboxY() + getHitboxHeight(), getHitboxWidth(), borderSize.get2(), 0, border.get2()); + Vera.renderer.drawRect(app, getEffectiveX(), getEffectiveY() + getEffectiveHeight(), getEffectiveWidth(), borderSize.get2(), 0, borderColor.get2()); if (borderSize.get4() > 0) { - Vera.renderer.drawRect(app, getHitboxX() + getHitboxWidth(), getHitboxY() + getHitboxHeight(), borderSize.get4(), borderSize.get2(), 0, border.get2()); + Vera.renderer.drawRect(app, getEffectiveX() + getEffectiveWidth(), getEffectiveY() + getEffectiveHeight(), borderSize.get4(), borderSize.get2(), 0, borderColor.get2()); } // Left - Vera.renderer.drawRect(app, getHitboxX() - borderSize.get3(), getHitboxY(), borderSize.get3(), getHitboxHeight(), 0, border.get3()); + Vera.renderer.drawRect(app, getEffectiveX() - borderSize.get3(), getEffectiveY(), borderSize.get3(), getEffectiveHeight(), 0, 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(app, getEffectiveX() - borderSize.get3(), getEffectiveY() + getEffectiveHeight(), borderSize.get3(), borderSize.get2(), 0, borderColor.get3()); } // Right - Vera.renderer.drawRect(app, getHitboxX() + getHitboxWidth(), getHitboxY(), borderSize.get4(), getHitboxHeight(), 0, border.get4()); + Vera.renderer.drawRect(app, getEffectiveX() + getEffectiveWidth(), getEffectiveY(), borderSize.get4(), getEffectiveHeight(), 0, 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(app, getEffectiveX() + getEffectiveWidth(), getEffectiveY() - borderSize.get1(), borderSize.get4(), borderSize.get1(), 0, borderColor.get4()); } } - public void setSize(int width, int height) { - setWidth(width); - setHeight(height); - } + public void renderOverlay() { + StyleState state = createStyleState(); - public void setSize(int all) { - setSize(all, all); - } - - public void move(int x, int y) { - this.x = x; - this.y = y; - } - - public void move(int both) { - move(both, both); + Vera.renderer.drawRect(app, getEffectiveX(), getEffectiveY(), getEffectiveWidth(), getEffectiveHeight(), 0, getStyle("overlay", state)); } public boolean isLeftClickDown() { @@ -197,10 +155,6 @@ public boolean isAnyMouseButtonDown() { return leftClickDown || middleClickDown || rightClickDown; } - public VeraApp getApp() { - return app; - } - public double getRotation() { return rotation; } @@ -209,7 +163,11 @@ public void rotate(double rotation) { this.rotation = rotation; } - public void update() {} + public void update() { + StyleState state = createStyleState(); + + app.setCursorShape(getStyle("cursor", state)); + } public boolean isHovered() { return hovered; @@ -218,220 +176,139 @@ 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(Events.Widget.HOVER); + else events.fire(Events.Widget.HOVER_LEAVE); } this.hovered = hovered; } public void onHover(Runnable runnable) { - registerEventExecutor("hover", runnable); + events.register(Events.Widget.HOVER, runnable); } public void onHoverLeave(Runnable runnable) { - registerEventExecutor("hover-leave", runnable); + events.register(Events.Widget.HOVER_LEAVE, runnable); } public void onLeftClick(Runnable runnable) { - registerEventExecutor("left-click", runnable); + events.register(Events.Widget.LEFT_CLICK, runnable); } public void onLeftClickRelease(Runnable runnable) { - registerEventExecutor("left-click-release", runnable); + events.register(Events.Widget.LEFT_CLICK_RELEASE, runnable); } public void onRightClick(Runnable runnable) { - registerEventExecutor("right-click", runnable); + events.register(Events.Widget.RIGHT_CLICK, runnable); } public void onRightClickRelease(Runnable runnable) { - registerEventExecutor("right-click-release", runnable); + events.register(Events.Widget.RIGHT_CLICK_RELEASE, runnable); } public void onMiddleClick(Runnable runnable) { - registerEventExecutor("middle-click", runnable); + events.register(Events.Widget.MIDDLE_CLICK, runnable); } public void onMiddleClickRelease(Runnable runnable) { - registerEventExecutor("middle-click-release", runnable); + events.register(Events.Widget.MIDDLE_CLICK_RELEASE, runnable); } public void onMouseScroll(VMouseScrollEvent runnable) { - registerEventExecutor("mouse-scroll", args -> runnable.run( + events.register(Events.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(Events.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(Events.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(Events.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(Events.Widget.DRAG_MIDDLE_CLICK, args -> runnable.run((VMouseDragEvent.Context) args[0])); } public void onFocusStateChange(Runnable runnable) { - registerEventExecutor("focus-state-change", runnable); + events.register(Events.Widget.FOCUS_STATE_CHANGE, runnable); } public void onFilesDropped(VFilesDroppedEvent runnable) { - registerEventExecutor("files-dropped", args -> runnable.run((List) args[0])); - } - - public void onMessage(VWidgetMessageEvent runnable) { - registerEventExecutor("widget-message", args -> runnable.run((VWidgetMessageEvent.Context) args[0])); + events.register(Events.Widget.FILES_DROPPED, args -> runnable.run((List) args[0])); } - public void sendMessage(VWidget widget, String type) { - sendMessage(widget, type, null); + public void onAnimationBegin(VAnimationBeginEvent runnable) { + events.register(Events.Animation.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 onAnimationUnwindBegin(VAnimationUnwindEvent runnable) { + events.register(Events.Animation.UNWIND_BEGIN, args -> runnable.run((VAnimation) args[0])); } - public void sendMessageAll(String type) { - sendMessageAll(type, null); + public void onAnimationRewindBegin(VAnimationRewindEvent runnable) { + events.register(Events.Animation.REWIND_BEGIN, args -> runnable.run((VAnimation) args[0])); } - 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 onAnimationFinish(VAnimationFinishEvent runnable) { + events.register(Events.Animation.FINISH, args -> runnable.run((VAnimation) args[0], (long) args[1])); } + @Override public void handleBuiltinEvent(String event, Object... args) { switch (event) { - case "left-click" -> { - if (shouldFocusOnClick()) { + case Events.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 Events.Widget.RIGHT_CLICK -> rightClickDown = true; + case Events.Widget.MIDDLE_CLICK -> middleClickDown = true; - case "hover" -> { - cursorBeforeHover = app.getCursorShape(); - app.setCursorShape(hoverCursor); - } + case Events.Widget.LEFT_CLICK_RELEASE -> clearLeftClickDown(); + case Events.Widget.RIGHT_CLICK_RELEASE -> clearRightClickDown(); + case Events.Widget.MIDDLE_CLICK_RELEASE -> clearMiddleClickDown(); - case "hover-leave" -> { + case Events.Widget.HOVER_LEAVE -> { clearLeftClickDown(); clearRightClickDown(); clearMiddleClickDown(); + } + } + } - if (cursorBeforeHover == null) break; + @Override + public void afterBuiltinEvent(String name, Object... args) { + updateIfNeeded(); + } - app.setCursorShape(cursorBeforeHover); - } + private void updateIfNeeded() { + StyleState state = createStyleState(); + if (state != prevStyleState) { + update(); + prevStyleState = 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() { @@ -443,14 +320,6 @@ public void setFocused(boolean focused) { 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) {} @@ -459,16 +328,21 @@ public void remove() { app.removeWidget(this); } + public T alsoAddClass(String clazz) { + classes.add(clazz); + return (T) this; + } + public T alsoAdd() { app.addWidget(this); return (T) this; } - public void addVisibilityCondition(Supplier condition) { - visibilityConditions.add(condition); - } + @Override + public T alsoAddTo(VLayout layout) { + super.alsoAddTo(layout); + alsoAdd(); - public boolean visibilityConditionsPassed() { - return visibilityConditions.parallelStream().allMatch(Supplier::get); + 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..f762fbef 100644 --- a/src/main/resources/mcvera.mixins.json +++ b/src/main/resources/mcvera.mixins.json @@ -3,13 +3,15 @@ "package": "net.snackbag.mcvera.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ - "MinecraftClientMixin", - "VeraAppMixin" + "MinecraftClientMixin" ], "injectors": { "defaultRequire": 1 }, "client": [ + "DrawContextMixin", + "GameRendererMixin", + "InGameHudMixin", "KeyboardMixin", "MouseMixin", "ParentElementMixin",