Dynamic Tooltips

Package:              com.codex.composer.api.v1.tooltips
Canonical Name: com.codex.composer.api.v1.tooltips.*

Composer offers a system for dynamically registering tooltips for items. It is an alternate to the fabric tooltip API and completely bypasses it, though both can be used in parallel. Composer's systems allow you to specify a check to make sure they are relevant, and a modifier to show them. If that key combination is not pressed, it will show the required button to show it.

Classes

TooltipContext

TooltipContext is passed on each render pass to the dynamic tooltip, and contains the item stack and held keys. These keys are defined in it as three booleans, but there does exist a Modifier enum with values SHIFT, CTRL and ALT, and a function which takes a TargetingContext and returns the related boolean.

DynamicTooltip

DynamicTooltip is the interface you implement. Inheritors must implement:

/* ? if minecraft is 1.21.4 or below */
void appendTooltip(TooltipContext context, List<Text> out);
/* ? if minecraft is 1.21.5 or above */
void appendTooltip(TooltipContext context, Consumer<Text> out);
/* If false, the appendTooltip method will not be called at all. There is a config
option to override this accessible from mod menu or through /configure. */
boolean isRelevant(TooltipContext context);
/* Where to inject the tooltip. The available values for this
change based on what minecraft version you are using. */
Location where();

Location

As mentioned above, specified where to inject the tooltip. The enum's values follow each other in order that they are in the item tooltip. Available values are:

enum Location {
HEAD,
AFTER_NAME,
AFTER_MAP_ID,
AFTER_ITEM_TOOLTIP,
// if minecraft is 1.21 or above
AFTER_JUKEBOX_PLAYABLE,
AFTER_TRIM,
//? if minecraft: >=1.20.6
AFTER_STORED_ENCHANTMENTS,
AFTER_ENCHANTMENTS,
AFTER_DYED_COLOR,
AFTER_LORE,
AFTER_ATTRIBUTE_MODIFIERS,
AFTER_UNBREAKABLE,
/* if minecraft is 1.21 or above */
AFTER_OMINOUS_BOTTLE_AMPLIFIER,
AFTER_SUSPICIOUS_STEW_EFFECTS, /* This is 1.21+ too */
/* if minecraft is 1.20.6 or above */
AFTER_CAN_BREAK,
AFTER_CAN_PLACE_ON,
//? }
AFTER_DURABILITY,
AFTER_ITEM_ID,
/* if minecraft is 1.20.6 or above */
AFTER_COMPONENTS,
/* if minecraft is 1.20.4 or below */
AFTER_TAGS,
AFTER_DISABLED_TEXT,
/* if minecraft is 1.21 or above */
AFTER_OPERATOR_WARNINGS,
TAIL,
/* This only appears in creative mode! */
AFTER_CREATIVE_TOOLTIP
}

Section & SectionBuilder

A "Section" represents a single collapsible block of tooltip content. It is constructed via SectionBuilder.create()1 and supports the following properties:

PropertyDescription
titleA translation key for the section header. Pass an empty string to omit it.
detailsHint text 'details'. Gets passed through Text.translatable() and inserted into "Press <button> to show <details>"
keyComboA function that returns a Modifier from a TooltipContext
contentA function that returns a list of Text instances that is used as the "main" content
childrenNested sections in this one. These can have different keybinds, and are part of the content block.
titleFormatFormatting applied to the title line
contentFormatFormatting applied to content lines (Gray by default)
hiddenFormatFormatting applied to the details hint (Gray by default).

DynamicTooltipRegistry

Singleton registry used to store dynamic tooltips. Has it's own deferred registry, it is recommended to use that.

Creating a Tooltip

The simplest way to create a dynamic tooltip is through extending SimpleDynamicTooltip, as it directly integrates with sections. Implementing DynamicTooltip is possible, but not recommended.

public class ItemTooltip extends SimpleDynamicTooltip {
@Override
public Section root() {
return SectionBuilder.create()
.title("example.tooltip.title")
.keyCombo(ctx -> Modifier.SHIFT)
.content(ctx -> List.of(
Text.translatable("example.tooltip.line1"),
Text.translatable("example.tooltip.line2")
))
.build();
}
@Override
public boolean isRelevant(TooltipContext context) {
return context.stack.isOf(ModItems.EXAMPLE_ITEM);
}
@Override
public Location where() {
return Location.AFTER_NAME;
}
}

Then, if you like simple things, use:

DeferredDynamicTooltipRegistry REGISTRY = new DeferredDynamicTooltipRegistry(ExampleMod.MOD_ID);
REGISTRY.add("tooltip", ItemTooltip::new);

Or, alternatively use the registry directly:

DynamicTooltipRegistry.getInstance().register(Identifier.of(ExampleMod.MOD_ID, "tooltip"), new MyItemTooltip());

Example Implementation

From Composer

public class SoulboundTooltip extends SimpleDynamicTooltip {
@Override
public Section root() {
Section droppable = SectionBuilder.create()
.title("")
.keyCombo(ctx -> Modifier.ALT)
.content(ctx -> List.of(Text.translatable(LanguageUtils.negate("composer.tooltips.soulbound.droppable", SoulboundComponent.canDropSoulbound(ctx.stack))))) /* "This item is (not) soulbound" */
.build();
return SectionBuilder.create()
.title("")
.details(Text.translatable("composer.tooltips.soulbound.details"))
.keyCombo(ctx -> SoulboundComponent.isSoulbound(ctx.stack) ? null : Modifier.SHIFT)
.content(ctx -> List.of(Text.translatable(LanguageUtils.negate("composer.tooltips.soulbound", SoulboundComponent.isSoulbound(ctx.stack))))) /* "This item is (not) droppable when soulbound." */
.children().push(droppable).end()
.build();
}
@Override
public boolean isRelevant(TooltipContext context) {
return SoulboundComponent.isSoulbound(context.stack); // Hide if not soulbound
}
@Override
public Location where() {
return Location.AFTER_NAME;
}
}

Footnotes

  1. SectionBuilder is generated by Constructive, don't come complaining that it's missing methods (though I did make Constructive but ignore that)