1
0
forked from Clones/Controlify

minor improvements to carousel

This commit is contained in:
isXander
2023-05-29 20:49:06 +01:00
parent 80869cb654
commit fce4dc37ec
3 changed files with 142 additions and 72 deletions

View File

@ -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.FrameLayout;
import net.minecraft.client.gui.layouts.GridLayout; import net.minecraft.client.gui.layouts.GridLayout;
import net.minecraft.client.gui.narration.NarratableEntry; 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.narration.NarrationElementOutput;
import net.minecraft.client.gui.navigation.FocusNavigationEvent; 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.navigation.ScreenRectangle;
import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen; import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
@ -41,11 +40,12 @@ public class ControllerCarouselScreen extends Screen {
private List<CarouselEntry> carouselEntries = null; private List<CarouselEntry> carouselEntries = null;
private int carouselIndex; private int carouselIndex;
private Animator animator; private Animator.AnimationInstance carouselAnimation = null;
private ControllerCarouselScreen(Screen parent) { private ControllerCarouselScreen(Screen parent) {
super(Component.translatable("controlify.gui.carousel.title")); super(Component.translatable("controlify.gui.carousel.title"));
this.parent = parent; this.parent = parent;
this.carouselIndex = Controlify.instance().getCurrentController().map(c -> ControllerManager.getConnectedControllers().indexOf(c)).orElse(0);
} }
public static Screen createConfigScreen(Screen parent) { public static Screen createConfigScreen(Screen parent) {
@ -88,15 +88,26 @@ public class ControllerCarouselScreen extends Screen {
} }
public void refreshControllers() { public void refreshControllers() {
Controller<?, ?> prevSelectedController;
if (carouselEntries != null) { if (carouselEntries != null) {
carouselEntries.forEach(this::removeWidget); carouselEntries.forEach(this::removeWidget);
prevSelectedController = carouselEntries.get(carouselIndex).controller;
} else {
prevSelectedController = null;
} }
carouselEntries = ControllerManager.getConnectedControllers().stream() carouselEntries = ControllerManager.getConnectedControllers().stream()
.map(c -> new CarouselEntry(c, this.width / 3, this.height - 66)) .map(c -> new CarouselEntry(c, this.width / 3, this.height - 66))
.peek(this::addRenderableWidget) .peek(this::addRenderableWidget)
.toList(); .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()) if (!carouselEntries.isEmpty())
carouselEntries.get(carouselIndex).overlayColor = 0; 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.blit(CreateWorldScreen.LIGHT_DIRT_BACKGROUND, 0, 0, 0, 0f, 0f, this.width, footerY, 32, 32);
graphics.setColor(1f, 1f, 1f, 1f); graphics.setColor(1f, 1f, 1f, 1f);
if (animator != null && !animator.isDone()) {
animator.tick(delta);
}
if (carouselEntries.isEmpty()) { if (carouselEntries.isEmpty()) {
graphics.drawCenteredString(font, Component.translatable("controlify.gui.carousel.no_controllers"), this.width / 2, (this.height - 36) / 2 - 10, 0xFFAAAAAA); 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); 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) { public void focusOnEntry(int index) {
if (animator != null && !animator.isDone()) if (carouselAnimation != null && !carouselAnimation.isDone())
return; return;
int diff = index - carouselIndex; int diff = index - carouselIndex;
@ -162,13 +152,14 @@ public class ControllerCarouselScreen extends Screen {
carouselIndex = index; 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) { for (CarouselEntry entry : carouselEntries) {
boolean selected = carouselEntries.indexOf(entry) == index; boolean selected = carouselEntries.indexOf(entry) == index;
animator.addConsumer(entry::setX, entry.getX(), entry.getX() + -diff * (this.width / 2f)); carouselAnimation.addConsumer(entry::setX, entry.getX(), entry.getX() + -diff * (this.width / 2f));
animator.addConsumer(entry::setY, entry.getY(), selected ? 20f : 10f); carouselAnimation.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(t -> entry.overlayColor = FastColor.ARGB32.lerp(t, entry.overlayColor, selected ? 0 : 0x90000000), 0f, 1f);
} }
Animator.INSTANCE.play(carouselAnimation);
} }
@Override @Override
@ -179,18 +170,23 @@ public class ControllerCarouselScreen extends Screen {
private class CarouselEntry extends AbstractContainerEventHandler implements Renderable, NarratableEntry { private class CarouselEntry extends AbstractContainerEventHandler implements Renderable, NarratableEntry {
private int x, y; private int x, y;
private final int width, height; private final int width, height;
private float translationX, translationY; private float translationX, translationY;
private final Controller<?, ?> controller; private final Controller<?, ?> controller;
private final boolean hasNickname; private final boolean hasNickname;
private Button useControllerButton; private final Button useControllerButton;
private Button settingsButton; private final Button settingsButton;
private ImmutableList<? extends GuiEventListener> children; private final ImmutableList<? extends GuiEventListener> children;
private boolean prevUse;
private float currentlyUsedPos;
private Animator.AnimationInstance currentlyUsedAnimation;
private int overlayColor = 0x90000000; private int overlayColor = 0x90000000;
private boolean hovered = false;
private CarouselEntry(Controller<?, ?> controller, int width, int height) { private CarouselEntry(Controller<?, ?> controller, int width, int height) {
this.width = width; this.width = width;
this.height = height; 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.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.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.children = ImmutableList.of(settingsButton, useControllerButton);
this.prevUse = isCurrentlyUsed();
this.currentlyUsedPos = prevUse ? 0 : -1;
} }
@Override @Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { 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().pushPose();
graphics.pose().translate(translationX, translationY, 0); graphics.pose().translate(translationX, translationY, 0);
@ -222,10 +225,15 @@ public class ControllerCarouselScreen extends Screen {
controller.config().customName = nickname; 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.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; int iconWidth = width - 6;
// buttons 4px padding top currently in use controller name image padding // 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().translate(x + width / 2 - iconSize / 2, y + font.lineHeight + 12 + iconHeight / 2 - iconSize / 2, 0);
graphics.pose().scale(iconSize / 64f, iconSize / 64f, 1); graphics.pose().scale(iconSize / 64f, iconSize / 64f, 1);
graphics.blit(controller.icon(), 0, 0, 0f, 0f, 64, 64, 64, 64); graphics.blit(controller.icon(), 0, 0, 0f, 0f, 64, 64, 64, 64);
graphics.pose().popPose();
graphics.pose().translate(0, 0, 1); graphics.pose().translate(0, 0, 1);
graphics.fill(x, y, x + width, y + height, overlayColor); graphics.fill(x, y, x + width, y + height, overlayColor);
graphics.pose().popPose(); 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 @Override
@ -248,7 +267,7 @@ public class ControllerCarouselScreen extends Screen {
if (mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height) { if (mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height) {
int index = carouselEntries.indexOf(this); int index = carouselEntries.indexOf(this);
if (index != carouselIndex) { if (index != carouselIndex) {
if (animator == null || animator.isDone()) if (carouselAnimation == null || carouselAnimation.isDone())
focusOnEntry(index); focusOnEntry(index);
return true; return true;
@ -304,7 +323,7 @@ public class ControllerCarouselScreen extends Screen {
@Nullable @Nullable
@Override @Override
public ComponentPath nextFocusPath(FocusNavigationEvent event) { public ComponentPath nextFocusPath(FocusNavigationEvent event) {
if (animator != null && !animator.isDone()) if (carouselAnimation != null && !carouselAnimation.isDone())
return null; return null;
return super.nextFocusPath(event); return super.nextFocusPath(event);
} }
@ -314,14 +333,19 @@ public class ControllerCarouselScreen extends Screen {
return new ScreenRectangle(x, y, width, height); return new ScreenRectangle(x, y, width, height);
} }
public boolean isCurrentlyUsed() {
return Controlify.instance().getCurrentController().orElse(null) == controller;
}
@Override @Override
public NarrationPriority narrationPriority() { public NarrationPriority narrationPriority() {
return NarrationPriority.NONE; return isFocused() ? NarrationPriority.FOCUSED : hovered ? NarrationPriority.HOVERED : NarrationPriority.NONE;
} }
@Override @Override
public void updateNarration(NarrationElementOutput builder) { 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."));
} }
} }
} }

View File

@ -5,6 +5,7 @@ import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.ControllerManager;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.gui.screen.BetaNoticeScreen; import dev.isxander.controlify.gui.screen.BetaNoticeScreen;
import dev.isxander.controlify.utils.Animator;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.ToastComponent; import net.minecraft.client.gui.components.toasts.ToastComponent;
import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen;
@ -57,4 +58,9 @@ public abstract class MinecraftMixin {
private void onMinecraftClose(CallbackInfo ci) { private void onMinecraftClose(CallbackInfo ci) {
ControllerManager.getConnectedControllers().forEach(Controller::close); 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());
}
} }

View File

@ -2,52 +2,92 @@ package dev.isxander.controlify.utils;
import net.minecraft.util.Mth; import net.minecraft.util.Mth;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
public class Animator { public final class Animator {
private final List<AnimationConsumer> animations; public static final Animator INSTANCE = new Animator();
private final UnaryOperator<Float> easingFunction;
private final int durationTicks;
private float time;
public Animator(int durationTicks, UnaryOperator<Float> easingFunction) { private final List<AnimationInstance> animations;
private Animator() {
this.animations = new ArrayList<>(); this.animations = new ArrayList<>();
this.easingFunction = easingFunction;
this.durationTicks = durationTicks;
} }
public void addConsumer(Consumer<Float> consumer, float start, float end) { public void progress(float deltaTime) {
animations.add(new AnimationConsumer(consumer, start, end)); animations.forEach(animation -> animation.tick(deltaTime));
animations.removeIf(AnimationInstance::isDone);
} }
public void addConsumer(Consumer<Integer> consumer, int start, int end) { public AnimationInstance play(AnimationInstance animation) {
animations.add(new AnimationConsumer(aFloat -> consumer.accept(aFloat.intValue()), start, end)); animations.add(animation);
return animation;
} }
public void tick(float deltaTime) { public static class AnimationInstance {
time += deltaTime; private final List<AnimationConsumer> animations;
if (time > durationTicks) { private final UnaryOperator<Float> easingFunction;
time = durationTicks; private final int durationTicks;
private float time;
private boolean done;
private final List<Runnable> callbacks = new ArrayList<>();
public AnimationInstance(int durationTicks, UnaryOperator<Float> easingFunction) {
this.animations = new ArrayList<>();
this.easingFunction = easingFunction;
this.durationTicks = durationTicks;
} }
updateConsumers();
}
private void updateConsumers() { public AnimationInstance addConsumer(Consumer<Float> consumer, float start, float end) {
animations.forEach(consumer -> { animations.add(new AnimationConsumer(consumer, start, end));
float progress = easingFunction.apply(time / durationTicks); return this;
float value = Mth.lerp(progress, consumer.start, consumer.end); }
consumer.consumer.accept(value);
});
}
public boolean isDone() { public AnimationInstance addConsumer(Consumer<Integer> consumer, int start, int end) {
return time >= durationTicks; animations.add(new AnimationConsumer(aFloat -> consumer.accept(aFloat.intValue()), start, end));
} return this;
}
private record AnimationConsumer(Consumer<Float> 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<Float> consumer, float start, float end) {
}
} }
} }