From 87855c8102d5921101806b65bf02e68bb75776ae Mon Sep 17 00:00:00 2001 From: Space Walker Date: Sun, 8 Feb 2026 19:45:10 +0100 Subject: [PATCH] initial work on API these are most of the elements modders will interact with - still missing is - json serdes - interoperability with vanilla (conversion between vanilla Text, and methods for e.g. rendering OSL components) --- libraries/text-components/build.gradle | 2 + libraries/text-components/gradle.properties | 6 + .../ornithemc/osl/text/api/ClickEvent.java | 56 +++ .../ornithemc/osl/text/api/Formatting.java | 68 ++++ .../ornithemc/osl/text/api/HoverEvent.java | 44 +++ .../net/ornithemc/osl/text/api/Style.java | 346 ++++++++++++++++++ .../net/ornithemc/osl/text/api/TextColor.java | 71 ++++ .../ornithemc/osl/text/api/TextComponent.java | 23 ++ .../osl/text/api/TextComponents.java | 38 ++ .../osl/text/impl/BaseTextComponent.java | 95 +++++ .../text/impl/FormattedStringResolver.java | 70 ++++ .../osl/text/impl/LiteralTextComponent.java | 19 + .../ornithemc/osl/text/impl/TextResolver.java | 9 + .../osl/text/impl/TextResolvers.java | 39 ++ .../text/impl/TranslatableTextComponent.java | 95 +++++ settings.gradle | 2 + 16 files changed, 983 insertions(+) create mode 100644 libraries/text-components/build.gradle create mode 100644 libraries/text-components/gradle.properties create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/api/ClickEvent.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/api/Formatting.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/api/HoverEvent.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/api/Style.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/api/TextColor.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/api/TextComponent.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/api/TextComponents.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/impl/BaseTextComponent.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/impl/FormattedStringResolver.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/impl/LiteralTextComponent.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/impl/TextResolver.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/impl/TextResolvers.java create mode 100644 libraries/text-components/src/main/java/net/ornithemc/osl/text/impl/TranslatableTextComponent.java diff --git a/libraries/text-components/build.gradle b/libraries/text-components/build.gradle new file mode 100644 index 00000000..9d10b1b6 --- /dev/null +++ b/libraries/text-components/build.gradle @@ -0,0 +1,2 @@ +setUpLibrary(project) + diff --git a/libraries/text-components/gradle.properties b/libraries/text-components/gradle.properties new file mode 100644 index 00000000..1d71ad08 --- /dev/null +++ b/libraries/text-components/gradle.properties @@ -0,0 +1,6 @@ +library_id = text-components +library_name = Text Components +library_description = Versatile text components. +library_version = 0.1.0-alpha.1 + +osl_dependencies = core:>=0.7.0 diff --git a/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/ClickEvent.java b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/ClickEvent.java new file mode 100644 index 00000000..f6b383b2 --- /dev/null +++ b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/ClickEvent.java @@ -0,0 +1,56 @@ +package net.ornithemc.osl.text.api; + +import java.util.Objects; + +public class ClickEvent { + + private final Action action; + private final String value; + + private ClickEvent(Action action, String value) { + this.action = action; + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ClickEvent)) { + return false; + } + ClickEvent event = (ClickEvent) o; + return this.action == event.action && Objects.equals(this.value, event.value); + } + + public Action getAction() { + return this.action; + } + + public String getValue() { + return this.value; + } + + public static ClickEvent openUrl(String url) { + return new ClickEvent(Action.OPEN_URL, url); + } + + public static ClickEvent runCommand(String command) { + return new ClickEvent(Action.RUN_COMMAND, command); + } + + public static ClickEvent suggestCommand(String command) { + return new ClickEvent(Action.SUGGEST_COMMAND, command); + } + + public static ClickEvent copyToClipboard(String text) { + return new ClickEvent(Action.COPY_TO_CLIPBOARD, text); + } + + public enum Action { + + OPEN_URL, RUN_COMMAND, SUGGEST_COMMAND, COPY_TO_CLIPBOARD + + } +} diff --git a/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/Formatting.java b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/Formatting.java new file mode 100644 index 00000000..1a577845 --- /dev/null +++ b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/Formatting.java @@ -0,0 +1,68 @@ +package net.ornithemc.osl.text.api; + +public enum Formatting { + + BLACK ('0', 0x000000), + DARK_BLUE ('1', 0x0000AA), + DARK_GREEN ('2', 0x00AA00), + DARK_AQUA ('3', 0x00AAAA), + DARK_RED ('4', 0xAA0000), + DARK_PURPLE ('5', 0xAA00AA), + GOLD ('6', 0xFFAA00), + GRAY ('7', 0xAAAAAA), + DARK_GRAY ('8', 0x555555), + BLUE ('9', 0x5555FF), + GREEN ('a', 0x55FF55), + AQUA ('b', 0x55FFFF), + RED ('c', 0xFF5555), + LIGHT_PURPLE ('d', 0xFF55FF), + YELLOW ('e', 0xFFFF55), + WHITE ('f', 0xFFFFFF), + OBFUSCATED ('k'), + BOLD ('l'), + STRIKETHROUGH('m'), + UNDERLINED ('n'), + ITALIC ('o'), + RESET ('r'); + + public static final char PREFIX = 'ยง'; + + final char code; + final Integer color; + + private Formatting(char code) { + this(code, null); + } + + private Formatting(char code, Integer color) { + this.code = code; + this.color = color; + } + + @Override + public String toString() { + return "" + PREFIX + this.code; + } + + public char getCode() { + return this.code; + } + + public boolean isColor() { + return this.color != null; + } + + public Integer getColor() { + return this.color; + } + + public static Formatting byCode(char code) { + for (Formatting f : Formatting.values()) { + if (f.code == code) { + return f; + } + } + + throw new IllegalStateException("unknown text formatting code " + code); + } +} diff --git a/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/HoverEvent.java b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/HoverEvent.java new file mode 100644 index 00000000..73cd7581 --- /dev/null +++ b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/HoverEvent.java @@ -0,0 +1,44 @@ +package net.ornithemc.osl.text.api; + +import java.util.Objects; + +public class HoverEvent { + + private final Action action; + private final Object value; + + private HoverEvent(Action action, Object value) { + this.action = action; + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HoverEvent)) { + return false; + } + HoverEvent event = (HoverEvent) o; + return this.action == event.action && Objects.equals(this.value, event.value); + } + + public Action getAction() { + return this.action; + } + + public T getValue() { + return (T) this.value; + } + + public static HoverEvent showText(TextComponent text) { + return new HoverEvent(Action.SHOW_TEXT, text); + } + + public enum Action { + + SHOW_TEXT + + } +} diff --git a/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/Style.java b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/Style.java new file mode 100644 index 00000000..c877a8d3 --- /dev/null +++ b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/Style.java @@ -0,0 +1,346 @@ +package net.ornithemc.osl.text.api; + +import java.util.Objects; + +public class Style { + + public static final Style EMPTY = new Style(null, null, null, null, null, null, null, null); + + private final TextColor color; + private final Boolean bold; + private final Boolean italic; + private final Boolean underlined; + private final Boolean strikethrough; + private final Boolean obfuscated; + private final ClickEvent clickEvent; + private final HoverEvent hoverEvent; + + private Style( + TextColor color, + Boolean bold, + Boolean italic, + Boolean underlined, + Boolean strikethrough, + Boolean obfuscated, + ClickEvent clickEvent, + HoverEvent hoverEvent + ) { + this.color = color; + this.bold = bold; + this.italic = italic; + this.underlined = underlined; + this.strikethrough = strikethrough; + this.obfuscated = obfuscated; + this.clickEvent = clickEvent; + this.hoverEvent = hoverEvent; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Style)) { + return false; + } + Style style = (Style) o; + return this.color == style.color + && this.bold == style.bold + && this.italic == style.italic + && this.obfuscated == style.obfuscated + && this.strikethrough == style.strikethrough + && this.underlined == style.underlined + && Objects.equals(this.clickEvent, style.clickEvent) + && Objects.equals(this.hoverEvent, style.hoverEvent); + } + + public TextColor getColor() { + return this.color; + } + + public boolean isBold() { + return this.bold == Boolean.TRUE; + } + + public boolean isItalic() { + return this.italic == Boolean.TRUE; + } + + public boolean isStrikethrough() { + return this.strikethrough == Boolean.TRUE; + } + + public boolean isUnderlined() { + return this.underlined == Boolean.TRUE; + } + + public boolean isObfuscated() { + return this.obfuscated == Boolean.TRUE; + } + + public boolean isEmpty() { + return this == EMPTY; + } + + public ClickEvent getClickEvent() { + return this.clickEvent; + } + + public HoverEvent getHoverEvent() { + return this.hoverEvent; + } + + private static Style checkEmptyAfterChange(Style style, T oldValue, T newValue) { + return oldValue != null && newValue == null && style.equals(EMPTY) ? EMPTY : style; + } + + public Style withColor(int color) { + return this.withColor(TextColor.of(color)); + } + + public Style withColor(Formatting formatting) { + return this.withColor(TextColor.fromFormatting(formatting)); + } + + public Style withColor(TextColor color) { + return Objects.equals(this.color, color) + ? this + : checkEmptyAfterChange( + new Style( + color, + this.bold, + this.italic, + this.underlined, + this.strikethrough, + this.obfuscated, + this.clickEvent, + this.hoverEvent + ), + this.color, + color + ); + } + + public Style withBold(Boolean bold) { + return Objects.equals(this.bold, bold) + ? this + : checkEmptyAfterChange( + new Style( + this.color, + bold, + this.italic, + this.underlined, + this.strikethrough, + this.obfuscated, + this.clickEvent, + this.hoverEvent + ), + this.bold, + bold + ); + } + + public Style withItalic(Boolean italic) { + return Objects.equals(this.italic, italic) + ? this + : checkEmptyAfterChange( + new Style( + this.color, + this.bold, + italic, + this.underlined, + this.strikethrough, + this.obfuscated, + this.clickEvent, + this.hoverEvent + ), + this.italic, + italic + ); + } + + public Style withUnderlined(Boolean underlined) { + return Objects.equals(this.underlined, underlined) + ? this + : checkEmptyAfterChange( + new Style( + this.color, + this.bold, + this.italic, + underlined, + this.strikethrough, + this.obfuscated, + this.clickEvent, + this.hoverEvent + ), + this.underlined, + underlined + ); + } + + public Style withStrikethrough(Boolean strikethrough) { + return Objects.equals(this.strikethrough, strikethrough) + ? this + : checkEmptyAfterChange( + new Style( + this.color, + this.bold, + this.italic, + this.underlined, + strikethrough, + this.obfuscated, + this.clickEvent, + this.hoverEvent + ), + this.strikethrough, + strikethrough + ); + } + + public Style withObfuscated(Boolean obfuscated) { + return Objects.equals(this.obfuscated, obfuscated) + ? this + : checkEmptyAfterChange( + new Style( + this.color, + this.bold, + this.italic, + this.underlined, + this.strikethrough, + obfuscated, + this.clickEvent, + this.hoverEvent + ), + this.obfuscated, + obfuscated + ); + } + + public Style withClickEvent(ClickEvent clickEvent) { + return Objects.equals(this.clickEvent, clickEvent) + ? this + : checkEmptyAfterChange( + new Style( + this.color, + this.bold, + this.italic, + this.underlined, + this.strikethrough, + this.obfuscated, + clickEvent, + this.hoverEvent + ), + this.clickEvent, + clickEvent + ); + } + + public Style withHoverEvent(HoverEvent hoverEvent) { + return Objects.equals(this.hoverEvent, hoverEvent) + ? this + : checkEmptyAfterChange( + new Style( + this.color, + this.bold, + this.italic, + this.underlined, + this.strikethrough, + this.obfuscated, + this.clickEvent, + hoverEvent + ), + this.hoverEvent, + hoverEvent + ); + } + + public Style withFormatting(Formatting... formattings) { + TextColor color = this.color; + Boolean bold = this.bold; + Boolean italic = this.italic; + Boolean strikethrough = this.strikethrough; + Boolean underlined = this.underlined; + Boolean obfuscated = this.obfuscated; + + for (Formatting formatting : formattings) { + switch (formatting) { + case RESET: + return EMPTY; + case OBFUSCATED: + obfuscated = true; + break; + case BOLD: + bold = true; + break; + case STRIKETHROUGH: + strikethrough = true; + break; + case UNDERLINED: + underlined = true; + break; + case ITALIC: + italic = true; + break; + default: + if (formatting.isColor()) { + color = TextColor.fromFormatting(formatting); + } + } + } + + return new Style( + color, + bold, + italic, + underlined, + strikethrough, + obfuscated, + this.clickEvent, + this.hoverEvent + ); + } + + public Style withStyle(Style style) { + if (this == EMPTY) { + return style; + } + if (style == EMPTY) { + return this; + } + return new Style( + this.color != null ? this.color : style.color, + this.bold != null ? this.bold : style.bold, + this.italic != null ? this.italic : style.italic, + this.underlined != null ? this.underlined : style.underlined, + this.strikethrough != null ? this.strikethrough : style.strikethrough, + this.obfuscated != null ? this.obfuscated : style.obfuscated, + this.clickEvent != null ? this.clickEvent : style.clickEvent, + this.hoverEvent != null ? this.hoverEvent : style.hoverEvent + ); + } + + public void apply(StringBuilder sb) { + if (this.color != null) { + Formatting formatting = this.color.getFormatting(); + + if (formatting != null) { + sb.append(formatting); + } + } + if (this.bold != null) { + sb.append(Formatting.BOLD); + } + if (this.italic != null) { + sb.append(Formatting.ITALIC); + } + if (this.underlined != null) { + sb.append(Formatting.UNDERLINED); + } + if (this.strikethrough != null) { + sb.append(Formatting.STRIKETHROUGH); + } + if (this.obfuscated != null) { + sb.append(Formatting.OBFUSCATED); + } + } +} diff --git a/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/TextColor.java b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/TextColor.java new file mode 100644 index 00000000..e5a5f710 --- /dev/null +++ b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/TextColor.java @@ -0,0 +1,71 @@ +package net.ornithemc.osl.text.api; + +import java.util.EnumMap; +import java.util.Map; + +public class TextColor { + + private static final Map BY_FORMATTING = new EnumMap<>(Formatting.class); + + public static final TextColor BLACK = new TextColor(Formatting.BLACK); + public static final TextColor DARK_BLUE = new TextColor(Formatting.DARK_BLUE); + public static final TextColor DARK_GREEN = new TextColor(Formatting.DARK_GREEN); + public static final TextColor DARK_AQUA = new TextColor(Formatting.DARK_AQUA); + public static final TextColor DARK_RED = new TextColor(Formatting.DARK_RED); + public static final TextColor DARK_PURPLE = new TextColor(Formatting.DARK_PURPLE); + public static final TextColor GOLD = new TextColor(Formatting.GOLD); + public static final TextColor GRAY = new TextColor(Formatting.GRAY); + public static final TextColor DARK_GRAY = new TextColor(Formatting.DARK_GRAY); + public static final TextColor BLUE = new TextColor(Formatting.BLUE); + public static final TextColor GREEN = new TextColor(Formatting.GREEN); + public static final TextColor AQUA = new TextColor(Formatting.AQUA); + public static final TextColor RED = new TextColor(Formatting.RED); + public static final TextColor LIGHT_PURPLE = new TextColor(Formatting.LIGHT_PURPLE); + public static final TextColor YELLOW = new TextColor(Formatting.YELLOW); + public static final TextColor WHITE = new TextColor(Formatting.WHITE); + + private final Formatting formatting; + private final int color; + + private TextColor(Formatting formatting) { + BY_FORMATTING.put(formatting, this); + + this.formatting = formatting; + this.color = formatting.color; + } + + private TextColor(int color) { + this.formatting = null; + this.color = color; + } + + public int getColor() { + return this.color; + } + + public boolean isFormatting() { + return this.formatting != null; + } + + public Formatting getFormatting() { + return this.formatting; + } + + public static TextColor of(int color) { + return new TextColor(color); + } + + public static TextColor fromFormatting(Formatting formatting) { + if (formatting.isColor()) { + TextColor color = BY_FORMATTING.get(formatting); + + if (color != null) { + return color; + } + + throw new IllegalStateException("could not resolve text color " + formatting.name()); + } else { + return null; + } + } +} diff --git a/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/TextComponent.java b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/TextComponent.java new file mode 100644 index 00000000..6bee5001 --- /dev/null +++ b/libraries/text-components/src/main/java/net/ornithemc/osl/text/api/TextComponent.java @@ -0,0 +1,23 @@ +package net.ornithemc.osl.text.api; + +import java.util.function.UnaryOperator; + +public interface TextComponent { + + Style getStyle(); + + TextComponent format(Formatting... formattings); + + TextComponent format(Style style); + + TextComponent format(UnaryOperator