Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add aligned text for tooltips to align multiple lines #1029

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
10 changes: 5 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,12 @@ dependencies {
modImplementation "com.terraformersmc:modmenu:${project.mod_menu_version}"

// REI
modCompileOnly "me.shedaniel:RoughlyEnoughItems-api-fabric:${project.rei_version}"
//modRuntimeOnly "me.shedaniel:RoughlyEnoughItems-fabric:${project.rei_version}"
// modCompileOnly "me.shedaniel:RoughlyEnoughItems-api-fabric:${project.rei_version}"
modImplementation "me.shedaniel:RoughlyEnoughItems-fabric:${project.rei_version}"

// EMI
modCompileOnly "dev.emi:emi-fabric:${project.emi_version}:api"
//modLocalRuntime "dev.emi:emi-fabric:${project.emi_version}"
// modCompileOnly "dev.emi:emi-fabric:${project.emi_version}:api"
modImplementation "dev.emi:emi-fabric:${project.emi_version}"

// JEI (Using modrinth repo since official release is in mojmap and doesn't work)
modCompileOnly "maven.modrinth:jei:${project.jei_version}-fabric"
Expand Down Expand Up @@ -286,4 +286,4 @@ publishing {
// The repositories here will be used for publishing your artifact, not for
// retrieving dependencies.
}
}
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ mod_menu_version = 12.0.0-beta.1
## REI (https://modrinth.com/mod/rei/versions?l=fabric)
rei_version = 17.0.789
## EMI (https://modrinth.com/mod/emi/versions)
emi_version = 1.1.10+1.21
emi_version = 1.1.16+1.21.1
## JEI (https://modrinth.com/mod/jei/versions)
jei_version = 19.8.4.113

Expand Down
61 changes: 61 additions & 0 deletions src/main/java/de/hysky/skyblocker/injected/AlignedText.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package de.hysky.skyblocker.injected;

import net.minecraft.text.MutableText;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public interface AlignedText {
/**
* <h3>
* Aligned Text
* </h3>
* <p>
* This method is used to display text at a certain x offset after the current text in tooltips.
* This allows for aligned text when used on multiple rows with the same offset.
* </p>
* <p>
* This method can be chained to achieve a grid-like layout in tooltips.
* </p>
* <h3>
* Styling
* </h3>
* <p>
* The way styling applies to aligned text is slightly different from normal text, where the styling of the parent text is applied to children as well
* (which causes almost all uses of text with formatting to be appended on an empty parent text when there is more than 1 style in the same line).
* </p>
* <p>
* For aligned text, each node has their own formatting and there is no style inheritance between them.
* </p>
* <p>
* However, each aligned text node can still have their own children elements like normal text,
* where the children will inherit the style of the parent and their text content will be appended to the parent.
* </p>
*
* @param text The text to render after this text
* @param xOffset The x offset to apply to the given {@code text},
* relative to the start of the text object this method is called upon.
* @return The {@code text} object passed in, for chaining purposes
*/
default @NotNull MutableText align(@NotNull MutableText text, int xOffset) {
return text;
}

default @Nullable MutableText getAlignedText() {
return null;
}

/**
* @return The x offset to apply to the text, or {@link Integer#MIN_VALUE } if there's no aligned text or 0 if first of the chain
*/
default int getXOffset() {
return Integer.MIN_VALUE;
}

default void setXOffset(int xOffset) {}

default MutableText getFirstOfChain() {
return null;
}

default void setFirstOfChain(MutableText text) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

@Mixin(DrawContext.class)
public abstract class DrawContextMixin {
@ModifyExpressionValue(method = "drawCooldownProgress", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/ItemCooldownManager;getCooldownProgress(Lnet/minecraft/item/ItemStack;F)F"))
private float skyblocker$modifyItemCooldown(float cooldownProgress, @Local(argsOnly = true) ItemStack stack) {
return Utils.isOnSkyblock() && ItemCooldowns.isOnCooldown(stack) ? ItemCooldowns.getItemCooldownEntry(stack).getRemainingCooldownPercent() : cooldownProgress;
}
@ModifyExpressionValue(method = "drawCooldownProgress", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/ItemCooldownManager;getCooldownProgress(Lnet/minecraft/item/ItemStack;F)F"))
private float skyblocker$modifyItemCooldown(float cooldownProgress, @Local(argsOnly = true) ItemStack stack) {
return Utils.isOnSkyblock() && ItemCooldowns.isOnCooldown(stack) ? ItemCooldowns.getItemCooldownEntry(stack).getRemainingCooldownPercent() : cooldownProgress;
}
}
106 changes: 106 additions & 0 deletions src/main/java/de/hysky/skyblocker/mixins/MutableTextMixin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package de.hysky.skyblocker.mixins;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import de.hysky.skyblocker.injected.AlignedText;
import de.hysky.skyblocker.utils.render.gui.AlignedOrderedText;
import net.minecraft.text.MutableText;
import net.minecraft.text.OrderedText;
import net.minecraft.text.StringVisitable;
import net.minecraft.text.Text;
import net.minecraft.util.Language;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;

import java.util.ArrayList;
import java.util.List;

@Mixin(MutableText.class)
public abstract class MutableTextMixin implements AlignedText, Text {
//There's an implicit linked list here, where each text has a reference to the next one.
//With the addition of firstOfChain it becomes a doubly linked list with the caveat of the reference of each text being the first of the chain rather than the previous element.
//There's no need for a real doubly linked list with references to the previous elements, as the only operation that needs to be done is to get the first element of the chain to render the whole chain properly.
@Unique
@Nullable
private MutableText alignedWith = null;
@Unique
private int xOffset = Integer.MIN_VALUE;

// Null if this is the first of the chain, not null otherwise & always points to the first of the aligned text chain
@Unique
private MutableText firstOfChain = null;

@Unique
Logger logger = LoggerFactory.getLogger("Skyblocker Mutable Text");

@Override
public @NotNull MutableText align(@NotNull MutableText text, int xOffset) {
this.alignedWith = text;
if (firstOfChain == null) {
text.setFirstOfChain((MutableText) (Object) this);
text.setXOffset(xOffset);
this.xOffset = 0;
} else {
text.setFirstOfChain(firstOfChain);
text.setXOffset(xOffset);
}
return text;
}

@Override
public @Nullable MutableText getAlignedText() {
return alignedWith;
}

@Override
public int getXOffset() {
return xOffset;
}

@Override
public void setXOffset(int xOffset) {
this.xOffset = xOffset;
}

@Override
public MutableText getFirstOfChain() {
return firstOfChain;
}

@Override
public void setFirstOfChain(MutableText text) {
firstOfChain = text;
}

@WrapOperation(method = "asOrderedText", at = @At(target = "Lnet/minecraft/util/Language;reorder(Lnet/minecraft/text/StringVisitable;)Lnet/minecraft/text/OrderedText;", value = "INVOKE"))
private OrderedText skyblocker$asOrderedText(Language instance, StringVisitable visitable, Operation<OrderedText> original) {
if (visitable instanceof MutableText mutableText) {
MutableText tmp = mutableText.getFirstOfChain();
if (tmp != null) {
List<AlignedOrderedText.Segment> segments = new ArrayList<>();
while (tmp != null) {
segments.add(new AlignedOrderedText.Segment(original.call(instance, tmp), tmp.getXOffset()));
tmp = tmp.getAlignedText();
}
return new AlignedOrderedText(segments);
} else { // This is the first of the chain
tmp = mutableText.getAlignedText();
if (tmp != null) {
List<AlignedOrderedText.Segment> segments = new ArrayList<>();
segments.add(new AlignedOrderedText.Segment(original.call(instance, mutableText), 0));
while (tmp != null) {
segments.add(new AlignedOrderedText.Segment(original.call(instance, tmp), tmp.getXOffset()));
tmp = tmp.getAlignedText();
}
return new AlignedOrderedText(segments);
}
}
}
return original.call(instance, visitable);
}
}
63 changes: 63 additions & 0 deletions src/main/java/de/hysky/skyblocker/mixins/TextHandlerMixin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package de.hysky.skyblocker.mixins;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import de.hysky.skyblocker.utils.render.gui.AlignedOrderedText;
import net.minecraft.client.font.TextHandler;
import net.minecraft.text.*;
import org.apache.commons.lang3.mutable.MutableFloat;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
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.List;
import java.util.function.BiConsumer;

@Mixin(TextHandler.class)
public abstract class TextHandlerMixin {
@Final
@Shadow
TextHandler.WidthRetriever widthRetriever;

@WrapOperation(method = "getWidth(Lnet/minecraft/text/OrderedText;)F", at = @At(value = "INVOKE", target = "Lnet/minecraft/text/OrderedText;accept(Lnet/minecraft/text/CharacterVisitor;)Z"))
private boolean skyblocker$getWidth(OrderedText instance, CharacterVisitor characterVisitor, Operation<Boolean> original, @Local MutableFloat mutableFloat) {
if (instance instanceof AlignedOrderedText alignedOrderedText) {
MutableFloat width = new MutableFloat();
List<AlignedOrderedText.Segment> segments = alignedOrderedText.segments();
float tmp = 0;
for (AlignedOrderedText.Segment segment : segments) {
tmp = segment.xOffset();
if (width.addAndGet(-tmp) < 0) width.setValue(tmp); // If the offset is greater than the current width, set the width to the offset to not allow clipping between segments
segment.text().accept((index, style, codePoint) -> { // This is a copied version of the original operation, but the width addition is done to our MutableFloat rather than the original
width.add(this.widthRetriever.getWidth(codePoint, style));
return true;
});
}

mutableFloat.setValue(width.getValue());
return true;
}
return original.call(instance, characterVisitor);
}

@Inject(method = "wrapLines(Lnet/minecraft/text/StringVisitable;ILnet/minecraft/text/Style;Ljava/util/function/BiConsumer;)V", at = @At("HEAD"), cancellable = true)
private void skyblocker$wrapLines(StringVisitable text, int maxWidth, Style style, BiConsumer<StringVisitable, Boolean> lineConsumer, CallbackInfo ci) {
if (text instanceof MutableText mutableText) {
switch (mutableText.getXOffset()) {
case 0 -> {
lineConsumer.accept(mutableText, false);
ci.cancel();
}
case Integer.MIN_VALUE -> {}
default -> {
lineConsumer.accept(mutableText.getFirstOfChain(), false);
ci.cancel();
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package de.hysky.skyblocker.mixins.jei;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import mezz.jei.fabric.platform.RenderHelper;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.text.MutableText;
import net.minecraft.text.OrderedText;
import net.minecraft.text.StringVisitable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;

import java.util.List;

@Mixin(value = RenderHelper.class, remap = false)
public abstract class RenderHelperMixin {
@WrapOperation(method = "lambda$renderTooltip$0", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/font/TextRenderer;wrapLines(Lnet/minecraft/text/StringVisitable;I)Ljava/util/List;"), require = 0)
private static List<OrderedText> skyblocker$renderTooltip(TextRenderer instance, StringVisitable text, int width, Operation<List<OrderedText>> original) {
if (text instanceof MutableText mutableText && mutableText.getXOffset() != Integer.MIN_VALUE) return List.of(mutableText.asOrderedText());
return original.call(instance, text, width);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package de.hysky.skyblocker.mixins.rei;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import me.shedaniel.rei.impl.client.gui.fabric.ScreenOverlayImplFabric;
import net.minecraft.client.font.TextHandler;
import net.minecraft.text.MutableText;
import net.minecraft.text.StringVisitable;
import net.minecraft.text.Style;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;

import java.util.List;

@Mixin(value = ScreenOverlayImplFabric.class)
public class ScreenOverlayImplFabricMixin {
@WrapOperation(method = "lambda$renderTooltipInner$0", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/font/TextHandler;wrapLines(Lnet/minecraft/text/StringVisitable;ILnet/minecraft/text/Style;)Ljava/util/List;"), require = 0)
private static List<StringVisitable> renderTooltipInner(TextHandler instance, StringVisitable text, int maxWidth, Style style, Operation<List<StringVisitable>> original) {
if (text instanceof MutableText mutableText && mutableText.getXOffset() != Integer.MIN_VALUE) return List.of();

return original.call(instance, text, maxWidth, style);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import de.hysky.skyblocker.utils.scheduler.Scheduler;
import net.minecraft.client.MinecraftClient;
import net.minecraft.item.ItemStack;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.slf4j.Logger;
Expand Down Expand Up @@ -43,11 +44,11 @@ public static void nullWarning() {
}
}

public static Text getCoinsMessage(double price, int count) {
public static MutableText getCoinsMessage(double price, int count) {
return getCoinsMessage(price, count, false);
}

public static Text getCoinsMessage(double price, int count, boolean preCounted) {
public static MutableText getCoinsMessage(double price, int count, boolean preCounted) {
// Format the price string once
String priceString = String.format(Locale.ENGLISH, "%1$,.1f", preCounted ? price / count : price);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import it.unimi.dsi.fastutil.Pair;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.slot.Slot;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.jetbrains.annotations.Nullable;
Expand All @@ -25,20 +24,18 @@ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text>
Pair<AccessoriesHelper.AccessoryReport, String> report = AccessoriesHelper.calculateReport4Accessory(internalID);

if (report.left() != AccessoriesHelper.AccessoryReport.INELIGIBLE) {
MutableText title = Text.literal(String.format("%-19s", "Accessory: ")).withColor(0xf57542);

Text stateText = switch (report.left()) {
case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN);
case IS_GREATER_TIER -> Text.literal("✦ Upgrade ").withColor(0x218bff).append(Text.literal(report.right()).withColor(0xf8f8ff));
case HAS_GREATER_TIER -> Text.literal("↑ Upgradable ").withColor(0xf8d048).append(Text.literal(report.right()).withColor(0xf8f8ff));
case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade ").formatted(Formatting.GRAY).append(Text.literal(report.right()).withColor(0xf8f8ff));
case MISSING -> Text.literal("✖ Missing ").formatted(Formatting.RED).append(Text.literal(report.right()).withColor(0xf8f8ff));

//Should never be the case
default -> Text.literal("? Unknown").formatted(Formatting.GRAY);
};

lines.add(title.append(stateText));
lines.add(Text.literal("Accessory:").withColor(0xf57542).align(
switch (report.left()) {
case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN);
case IS_GREATER_TIER -> Text.literal("✦ Upgrade ").withColor(0x218bff).append(Text.literal(report.right()).withColor(0xf8f8ff));
case HAS_GREATER_TIER -> Text.literal("↑ Upgradable ").withColor(0xf8d048).append(Text.literal(report.right()).withColor(0xf8f8ff));
case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade ").formatted(Formatting.GRAY).append(Text.literal(report.right()).withColor(0xf8f8ff));
case MISSING -> Text.literal("✖ Missing ").formatted(Formatting.RED).append(Text.literal(report.right()).withColor(0xf8f8ff));

//Should never be the case
default -> Text.literal("? Unknown").formatted(Formatting.GRAY);
}, 100));
}
}
}
Expand Down
Loading
Loading