From fce4dc37ec6820cdfe5c67c7a29b3b788b56d9d1 Mon Sep 17 00:00:00 2001 From: isXander Date: Mon, 29 May 2023 20:49:06 +0100 Subject: [PATCH] minor improvements to carousel --- .../config/gui/ControllerCarouselScreen.java | 104 +++++++++++------- .../mixins/core/MinecraftMixin.java | 6 + .../isxander/controlify/utils/Animator.java | 104 ++++++++++++------ 3 files changed, 142 insertions(+), 72 deletions(-) diff --git a/src/main/java/dev/isxander/controlify/config/gui/ControllerCarouselScreen.java b/src/main/java/dev/isxander/controlify/config/gui/ControllerCarouselScreen.java index bb35632..b20b7fb 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/ControllerCarouselScreen.java +++ b/src/main/java/dev/isxander/controlify/config/gui/ControllerCarouselScreen.java @@ -18,10 +18,9 @@ import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.layouts.FrameLayout; import net.minecraft.client.gui.layouts.GridLayout; import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarratedElementType; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.FocusNavigationEvent; -import net.minecraft.client.gui.navigation.ScreenAxis; -import net.minecraft.client.gui.navigation.ScreenDirection; import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen; @@ -41,11 +40,12 @@ public class ControllerCarouselScreen extends Screen { private List carouselEntries = null; private int carouselIndex; - private Animator animator; + private Animator.AnimationInstance carouselAnimation = null; private ControllerCarouselScreen(Screen parent) { super(Component.translatable("controlify.gui.carousel.title")); this.parent = parent; + this.carouselIndex = Controlify.instance().getCurrentController().map(c -> ControllerManager.getConnectedControllers().indexOf(c)).orElse(0); } public static Screen createConfigScreen(Screen parent) { @@ -88,15 +88,26 @@ public class ControllerCarouselScreen extends Screen { } public void refreshControllers() { + Controller prevSelectedController; if (carouselEntries != null) { carouselEntries.forEach(this::removeWidget); + prevSelectedController = carouselEntries.get(carouselIndex).controller; + } else { + prevSelectedController = null; } carouselEntries = ControllerManager.getConnectedControllers().stream() .map(c -> new CarouselEntry(c, this.width / 3, this.height - 66)) .peek(this::addRenderableWidget) .toList(); - carouselIndex = Controlify.instance().getCurrentController().map(c -> ControllerManager.getConnectedControllers().indexOf(c)).orElse(0); + carouselIndex = carouselEntries.stream() + .filter(e -> e.controller == prevSelectedController) + .findFirst() + .map(carouselEntries::indexOf) + .orElse(Controlify.instance().getCurrentController() + .map(c -> ControllerManager.getConnectedControllers().indexOf(c)) + .orElse(0) + ); if (!carouselEntries.isEmpty()) carouselEntries.get(carouselIndex).overlayColor = 0; @@ -119,10 +130,6 @@ public class ControllerCarouselScreen extends Screen { graphics.blit(CreateWorldScreen.LIGHT_DIRT_BACKGROUND, 0, 0, 0, 0f, 0f, this.width, footerY, 32, 32); graphics.setColor(1f, 1f, 1f, 1f); - if (animator != null && !animator.isDone()) { - animator.tick(delta); - } - if (carouselEntries.isEmpty()) { graphics.drawCenteredString(font, Component.translatable("controlify.gui.carousel.no_controllers"), this.width / 2, (this.height - 36) / 2 - 10, 0xFFAAAAAA); } @@ -136,25 +143,8 @@ public class ControllerCarouselScreen extends Screen { graphics.blit(CreateWorldScreen.LIGHT_DIRT_BACKGROUND, 0, 0, 0, 0.0F, 0.0F, this.width, this.height, scale, scale); } - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - switch (keyCode) { - case InputConstants.KEY_RIGHT -> { - if (carouselEntries.stream().anyMatch(CarouselEntry::isFocused)) - focusOnEntry(Math.min(carouselEntries.size() - 1, carouselIndex + 1)); - return true; - } - case InputConstants.KEY_LEFT -> { - if (carouselEntries.stream().anyMatch(CarouselEntry::isFocused)) - focusOnEntry(Math.max(0, carouselIndex - 1)); - return true; - } - } - return super.keyPressed(keyCode, scanCode, modifiers); - } - public void focusOnEntry(int index) { - if (animator != null && !animator.isDone()) + if (carouselAnimation != null && !carouselAnimation.isDone()) return; int diff = index - carouselIndex; @@ -162,13 +152,14 @@ public class ControllerCarouselScreen extends Screen { carouselIndex = index; - animator = new Animator(10, x -> x < 0.5f ? 4 * x * x * x : 1 - (float)Math.pow(-2 * x + 2, 3) / 2); + carouselAnimation = new Animator.AnimationInstance(10, x -> x < 0.5f ? 4 * x * x * x : 1 - (float)Math.pow(-2 * x + 2, 3) / 2); for (CarouselEntry entry : carouselEntries) { boolean selected = carouselEntries.indexOf(entry) == index; - animator.addConsumer(entry::setX, entry.getX(), entry.getX() + -diff * (this.width / 2f)); - animator.addConsumer(entry::setY, entry.getY(), selected ? 20f : 10f); - animator.addConsumer(t -> entry.overlayColor = FastColor.ARGB32.lerp(t, entry.overlayColor, selected ? 0 : 0x90000000), 0f, 1f); + carouselAnimation.addConsumer(entry::setX, entry.getX(), entry.getX() + -diff * (this.width / 2f)); + carouselAnimation.addConsumer(entry::setY, entry.getY(), selected ? 20f : 10f); + carouselAnimation.addConsumer(t -> entry.overlayColor = FastColor.ARGB32.lerp(t, entry.overlayColor, selected ? 0 : 0x90000000), 0f, 1f); } + Animator.INSTANCE.play(carouselAnimation); } @Override @@ -179,18 +170,23 @@ public class ControllerCarouselScreen extends Screen { private class CarouselEntry extends AbstractContainerEventHandler implements Renderable, NarratableEntry { private int x, y; private final int width, height; - private float translationX, translationY; private final Controller controller; private final boolean hasNickname; - private Button useControllerButton; - private Button settingsButton; - private ImmutableList children; + private final Button useControllerButton; + private final Button settingsButton; + private final ImmutableList children; + + private boolean prevUse; + private float currentlyUsedPos; + private Animator.AnimationInstance currentlyUsedAnimation; private int overlayColor = 0x90000000; + private boolean hovered = false; + private CarouselEntry(Controller controller, int width, int height) { this.width = width; this.height = height; @@ -201,10 +197,17 @@ public class ControllerCarouselScreen extends Screen { this.settingsButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.settings"), btn -> minecraft.setScreen(ControllerConfigGui.generateConfigScreen(ControllerCarouselScreen.this, controller))).width(getWidth() / 2 - 4).build(); this.useControllerButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.use"), btn -> Controlify.instance().setCurrentController(controller)).width(settingsButton.getWidth()).build(); this.children = ImmutableList.of(settingsButton, useControllerButton); + + this.prevUse = isCurrentlyUsed(); + this.currentlyUsedPos = prevUse ? 0 : -1; } @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + hovered = isMouseOver(mouseX, mouseY); + + graphics.enableScissor(x, y, x + width + (translationX > 0 ? 1 : 0), y + height + (translationY > 0 ? 1 : 0)); + graphics.pose().pushPose(); graphics.pose().translate(translationX, translationY, 0); @@ -222,10 +225,15 @@ public class ControllerCarouselScreen extends Screen { controller.config().customName = nickname; } - if (Controlify.instance().getCurrentController().orElse(null) == controller) { + Component currentlyInUseText = Component.translatable("controlify.gui.carousel.entry.in_use").withStyle(ChatFormatting.GREEN); + graphics.pose().pushPose(); + graphics.pose().translate((4 + 9 + 4 + font.width(currentlyInUseText)) * currentlyUsedPos, 0, 0); + + if (currentlyUsedPos > -1) { graphics.blit(CHECKMARK, x + 4, y + 4, 0f, 0f, 9, 8, 9, 8); - graphics.drawString(font, Component.translatable("controlify.gui.carousel.entry.in_use").withStyle(ChatFormatting.GREEN), x + 17, y + 4, -1); + graphics.drawString(font, currentlyInUseText, x + 17, y + 4, -1); } + graphics.pose().popPose(); int iconWidth = width - 6; // buttons 4px padding top currently in use controller name image padding @@ -236,11 +244,22 @@ public class ControllerCarouselScreen extends Screen { graphics.pose().translate(x + width / 2 - iconSize / 2, y + font.lineHeight + 12 + iconHeight / 2 - iconSize / 2, 0); graphics.pose().scale(iconSize / 64f, iconSize / 64f, 1); graphics.blit(controller.icon(), 0, 0, 0f, 0f, 64, 64, 64, 64); + graphics.pose().popPose(); graphics.pose().translate(0, 0, 1); graphics.fill(x, y, x + width, y + height, overlayColor); graphics.pose().popPose(); + + graphics.disableScissor(); + + if (prevUse != isCurrentlyUsed()) { + if (currentlyUsedAnimation != null) + currentlyUsedAnimation.finish(); + currentlyUsedAnimation = Animator.INSTANCE.play(new Animator.AnimationInstance(20, t -> 1 - (float)Math.pow(1 - t, 5)) + .addConsumer(t -> currentlyUsedPos = t, currentlyUsedPos, isCurrentlyUsed() ? 0 : -1)); + } + prevUse = isCurrentlyUsed(); } @Override @@ -248,7 +267,7 @@ public class ControllerCarouselScreen extends Screen { if (mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height) { int index = carouselEntries.indexOf(this); if (index != carouselIndex) { - if (animator == null || animator.isDone()) + if (carouselAnimation == null || carouselAnimation.isDone()) focusOnEntry(index); return true; @@ -304,7 +323,7 @@ public class ControllerCarouselScreen extends Screen { @Nullable @Override public ComponentPath nextFocusPath(FocusNavigationEvent event) { - if (animator != null && !animator.isDone()) + if (carouselAnimation != null && !carouselAnimation.isDone()) return null; return super.nextFocusPath(event); } @@ -314,14 +333,19 @@ public class ControllerCarouselScreen extends Screen { return new ScreenRectangle(x, y, width, height); } + public boolean isCurrentlyUsed() { + return Controlify.instance().getCurrentController().orElse(null) == controller; + } + @Override public NarrationPriority narrationPriority() { - return NarrationPriority.NONE; + return isFocused() ? NarrationPriority.FOCUSED : hovered ? NarrationPriority.HOVERED : NarrationPriority.NONE; } @Override public void updateNarration(NarrationElementOutput builder) { - + builder.add(NarratedElementType.TITLE, controller.name()); + builder.add(NarratedElementType.USAGE, Component.literal("Left arrow to go to previous controller, right arrow to go to next controller.")); } } } diff --git a/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java b/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java index 4c5da71..4bcfb51 100644 --- a/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/core/MinecraftMixin.java @@ -5,6 +5,7 @@ import dev.isxander.controlify.Controlify; import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.gui.screen.BetaNoticeScreen; +import dev.isxander.controlify.utils.Animator; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.toasts.ToastComponent; import net.minecraft.client.gui.screens.Screen; @@ -57,4 +58,9 @@ public abstract class MinecraftMixin { private void onMinecraftClose(CallbackInfo ci) { ControllerManager.getConnectedControllers().forEach(Controller::close); } + + @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;render(FJZ)V")) + private void tickAnimator(boolean tick, CallbackInfo ci) { + Animator.INSTANCE.progress(this.getDeltaFrameTime()); + } } diff --git a/src/main/java/dev/isxander/controlify/utils/Animator.java b/src/main/java/dev/isxander/controlify/utils/Animator.java index cdafad5..fc85fc3 100644 --- a/src/main/java/dev/isxander/controlify/utils/Animator.java +++ b/src/main/java/dev/isxander/controlify/utils/Animator.java @@ -2,52 +2,92 @@ package dev.isxander.controlify.utils; import net.minecraft.util.Mth; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; import java.util.function.Consumer; import java.util.function.UnaryOperator; -public class Animator { - private final List animations; - private final UnaryOperator easingFunction; - private final int durationTicks; - private float time; +public final class Animator { + public static final Animator INSTANCE = new Animator(); - public Animator(int durationTicks, UnaryOperator easingFunction) { + private final List animations; + + private Animator() { this.animations = new ArrayList<>(); - this.easingFunction = easingFunction; - this.durationTicks = durationTicks; } - public void addConsumer(Consumer consumer, float start, float end) { - animations.add(new AnimationConsumer(consumer, start, end)); + public void progress(float deltaTime) { + animations.forEach(animation -> animation.tick(deltaTime)); + animations.removeIf(AnimationInstance::isDone); } - public void addConsumer(Consumer consumer, int start, int end) { - animations.add(new AnimationConsumer(aFloat -> consumer.accept(aFloat.intValue()), start, end)); + public AnimationInstance play(AnimationInstance animation) { + animations.add(animation); + return animation; } - public void tick(float deltaTime) { - time += deltaTime; - if (time > durationTicks) { - time = durationTicks; + public static class AnimationInstance { + private final List animations; + private final UnaryOperator easingFunction; + private final int durationTicks; + private float time; + private boolean done; + private final List callbacks = new ArrayList<>(); + + public AnimationInstance(int durationTicks, UnaryOperator easingFunction) { + this.animations = new ArrayList<>(); + this.easingFunction = easingFunction; + this.durationTicks = durationTicks; } - updateConsumers(); - } - private void updateConsumers() { - animations.forEach(consumer -> { - float progress = easingFunction.apply(time / durationTicks); - float value = Mth.lerp(progress, consumer.start, consumer.end); - consumer.consumer.accept(value); - }); - } + public AnimationInstance addConsumer(Consumer consumer, float start, float end) { + animations.add(new AnimationConsumer(consumer, start, end)); + return this; + } - public boolean isDone() { - return time >= durationTicks; - } + public AnimationInstance addConsumer(Consumer consumer, int start, int end) { + animations.add(new AnimationConsumer(aFloat -> consumer.accept(aFloat.intValue()), start, end)); + return this; + } - private record AnimationConsumer(Consumer consumer, float start, float end) { + public AnimationInstance onComplete(Runnable callback) { + callbacks.add(callback); + return this; + } + + private void tick(float deltaTime) { + time += deltaTime; + if (time > durationTicks) { + if (!done) { + callbacks.removeIf(callback -> { + callback.run(); + return true; + }); + } + done = true; + time = durationTicks; + } + + updateConsumers(); + } + + private void updateConsumers() { + animations.forEach(consumer -> { + float progress = easingFunction.apply(time / durationTicks); + float value = Mth.lerp(progress, consumer.start, consumer.end); + consumer.consumer.accept(value); + }); + } + + public void finish() { + time = durationTicks; + updateConsumers(); + } + + public boolean isDone() { + return done; + } + + private record AnimationConsumer(Consumer consumer, float start, float end) { + } } }