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

feat(api): virtual components #842

Open
wants to merge 12 commits into
base: main/4
Choose a base branch
from
73 changes: 73 additions & 0 deletions api/src/main/java/net/kyori/adventure/text/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,79 @@ public interface Component extends ComponentBuilderApplicable, ComponentLike, Ex
return text(String.valueOf(value), color, decorations);
}

/*
* --------------------------
* ---- VirtualComponent ----
* --------------------------
*/

/**
* Creates a virtual component.
*
* @param <C> the context type
* @param contextType the context type
* @param renderer the renderer
* @return a virtual component
* @since 4.16.0
*/
@Contract(value = "_, _ -> new", pure = true)
static <C> @NotNull VirtualComponent virtual(final @NotNull Class<C> contextType, final @NotNull VirtualComponentRenderer<C> renderer) {
requireNonNull(contextType, "context type");
requireNonNull(renderer, "renderer");
return VirtualComponentImpl.createVirtual(contextType, renderer);
}

/**
* Creates a virtual component with a value.
*
* @param <C> the context type
* @param contextType the context type
* @param renderer the renderer
* @param style the style
* @return a virtual component
* @since 4.16.0
*/
@Contract(value = "_, _, _ -> new", pure = true)
static <C> @NotNull VirtualComponent virtual(final @NotNull Class<C> contextType, final @NotNull VirtualComponentRenderer<C> renderer, final @NotNull Style style) {
requireNonNull(contextType, "context type");
requireNonNull(renderer, "renderer");
return VirtualComponentImpl.createVirtual(contextType, renderer, Collections.emptyList(), style);
}

/**
* Creates a virtual component with a value.
*
* @param <C> the context type
* @param contextType the context type
* @param renderer the renderer
* @param style the style elements
* @return a virtual component
* @since 4.16.0
*/
@Contract(value = "_, _, _ -> new", pure = true)
static <C> @NotNull VirtualComponent virtual(final @NotNull Class<C> contextType, final @NotNull VirtualComponentRenderer<C> renderer, final @NotNull StyleBuilderApplicable... style) {
requireNonNull(contextType, "context type");
requireNonNull(renderer, "renderer");
return VirtualComponentImpl.createVirtual(contextType, renderer, Collections.emptyList(), Style.style(style));
}

/**
* Creates a virtual component with a value.
*
* @param <C> the context type
* @param contextType the context type
* @param renderer the renderer
* @param style the style elements
* @return a virtual component
* @since 4.16.0
*/
@Contract(value = "_, _, _ -> new", pure = true)
static <C> @NotNull VirtualComponent virtual(final @NotNull Class<C> contextType, final @NotNull VirtualComponentRenderer<C> renderer, final @NotNull Iterable<StyleBuilderApplicable> style) {
requireNonNull(contextType, "context type");
requireNonNull(renderer, "renderer");
return VirtualComponentImpl.createVirtual(contextType, renderer, Collections.emptyList(), Style.style(style));
}

/*
* -------------------------------
* ---- TranslatableComponent ----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ static Component compact(final @NotNull Component self, final @Nullable Style pa
}

// if there is only one child, check if self a useless empty component
if (childrenSize == 1 && optimized instanceof TextComponent) {
if (childrenSize == 1 && isText(optimized)) {
final TextComponent textComponent = (TextComponent) optimized;

if (textComponent.content().isEmpty()) {
Expand All @@ -87,7 +87,7 @@ static Component compact(final @NotNull Component self, final @Nullable Style pa
child = compact(child, childParentStyle);

// ignore useless empty children (regardless of its style)
if (child.children().isEmpty() && child instanceof TextComponent) {
if (child.children().isEmpty() && isText(child)) {
final TextComponent textComponent = (TextComponent) child;

if (textComponent.content().isEmpty()) {
Expand All @@ -99,12 +99,12 @@ static Component compact(final @NotNull Component self, final @Nullable Style pa
}

// try to merge children into this parent component
if (optimized instanceof TextComponent) {
if (isText(optimized)) {
while (!childrenToAppend.isEmpty()) {
final Component child = childrenToAppend.get(0);
final Style childStyle = child.style().merge(childParentStyle, Style.Merge.Strategy.IF_ABSENT_ON_TARGET);

if (child instanceof TextComponent && Objects.equals(childStyle, childParentStyle)) {
if (isText(child) && Objects.equals(childStyle, childParentStyle)) {
// merge child components into the parent if they are a text component with the same effective style
// in context of their parent style
optimized = joinText((TextComponent) optimized, (TextComponent) child);
Expand All @@ -125,7 +125,7 @@ static Component compact(final @NotNull Component self, final @Nullable Style pa
final Component child = childrenToAppend.get(i);
final Component neighbor = childrenToAppend.get(i + 1);

if (child.children().isEmpty() && child instanceof TextComponent && neighbor instanceof TextComponent) {
if (child.children().isEmpty() && isText(child) && isText(neighbor)) {
// calculate the children's styles in context of their parent style
final Style childStyle = child.style().merge(childParentStyle, Style.Merge.Strategy.IF_ABSENT_ON_TARGET);
final Style neighborStyle = neighbor.style().merge(childParentStyle, Style.Merge.Strategy.IF_ABSENT_ON_TARGET);
Expand Down Expand Up @@ -162,7 +162,7 @@ static Component compact(final @NotNull Component self, final @Nullable Style pa
* @return true if the provided component is blank, false otherwise
*/
private static boolean isBlank(final Component component) {
if (component instanceof TextComponent) {
if (isText(component)) {
final TextComponent textComponent = (TextComponent) component;

final String content = textComponent.content();
Expand Down Expand Up @@ -215,4 +215,8 @@ private static boolean isBlank(final Component component) {
private static TextComponent joinText(final TextComponent one, final TextComponent two) {
return TextComponentImpl.create(two.children(), one.style(), one.content() + two.content());
}

private static boolean isText(final Component component) {
return component instanceof TextComponent && !(component instanceof VirtualComponent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

import static java.util.Objects.requireNonNull;

final class TextComponentImpl extends AbstractComponent implements TextComponent {
class TextComponentImpl extends AbstractComponent implements TextComponent {
private static final boolean WARN_WHEN_LEGACY_FORMATTING_DETECTED = Boolean.TRUE.equals(AdventureProperties.TEXT_WARN_WHEN_LEGACY_FORMATTING_DETECTED.value());
@VisibleForTesting
static final char SECTION_CHAR = '§';
Expand All @@ -56,6 +56,10 @@ static TextComponent create(final @NotNull List<? extends ComponentLike> childre
);
}

TextComponent create0(final @NotNull List<? extends ComponentLike> children, final @NotNull Style style, final @NotNull String content) {
return create(children, style, content);
}

private static @NotNull TextComponent createDirect(final @NotNull String content) {
return new TextComponentImpl(Collections.emptyList(), Style.empty(), content);
}
Expand Down Expand Up @@ -90,17 +94,17 @@ static TextComponent create(final @NotNull List<? extends ComponentLike> childre
@Override
public @NotNull TextComponent content(final @NotNull String content) {
if (Objects.equals(this.content, content)) return this;
return create(this.children, this.style, content);
return this.create0(this.children, this.style, content);
}

@Override
public @NotNull TextComponent children(final @NotNull List<? extends ComponentLike> children) {
return create(children, this.style, this.content);
return this.create0(children, this.style, this.content);
}

@Override
public @NotNull TextComponent style(final @NotNull Style style) {
return create(this.children, style, this.content);
return this.create0(this.children, style, this.content);
}

@Override
Expand Down Expand Up @@ -129,7 +133,7 @@ public String toString() {
return new BuilderImpl(this);
}

static final class BuilderImpl extends AbstractComponentBuilder<TextComponent, Builder> implements TextComponent.Builder {
static class BuilderImpl extends AbstractComponentBuilder<TextComponent, Builder> implements TextComponent.Builder {
/*
* We default to an empty string to avoid needing to manually set the
* content of a newly-created builder when we only want to append other
Expand Down
51 changes: 51 additions & 0 deletions api/src/main/java/net/kyori/adventure/text/VirtualComponent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2017-2024 KyoriPowered
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.kyori.adventure.text;

import org.jetbrains.annotations.NotNull;

/**
* A virtual component.
*
* <p>This component type is transient, and not guaranteed to survive during any sort of transformations or serialization.</p>
*
* @since 4.16.0
*/
public interface VirtualComponent extends TextComponent {
/**
* Gets the renderer context type.
*
* @return the renderer context type
* @since 4.16.0
*/
@NotNull Class<?> contextType();

/**
* Gets the renderer.
*
* @return the renderer
* @since 4.16.0
*/
@NotNull VirtualComponentRenderer<?> renderer();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2017-2024 KyoriPowered
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.kyori.adventure.text;

import java.util.Collections;
import java.util.List;
import net.kyori.adventure.text.format.Style;
import org.jetbrains.annotations.NotNull;

final class VirtualComponentImpl extends TextComponentImpl implements VirtualComponent {
static <C> VirtualComponent createVirtual(final @NotNull Class<C> contextType, final @NotNull VirtualComponentRenderer<?> renderer) {
return createVirtual(contextType, renderer, Collections.emptyList(), Style.empty());
}

static <C> VirtualComponent createVirtual(final @NotNull Class<C> contextType, final @NotNull VirtualComponentRenderer<?> renderer, final List<? extends ComponentLike> children, final Style style) {
final List<Component> filteredChildren = ComponentLike.asComponents(children, IS_NOT_EMPTY);

return new VirtualComponentImpl(filteredChildren, style, "", contextType, renderer);
}

private final Class<?> contextType;
private final VirtualComponentRenderer<?> renderer;

private VirtualComponentImpl(final @NotNull List<Component> children, final @NotNull Style style, final @NotNull String content, final @NotNull Class<?> contextType, final @NotNull VirtualComponentRenderer<?> renderer) {
super(children, style, content);
this.contextType = contextType;
this.renderer = renderer;
}

@Override
VirtualComponent create0(final @NotNull List<? extends ComponentLike> children, final @NotNull Style style, final @NotNull String content) {
return new VirtualComponentImpl(ComponentLike.asComponents(children, IS_NOT_EMPTY), style, content, this.contextType, this.renderer);
}

@Override
public @NotNull Class<?> contextType() {
return this.contextType;
}

@Override
public @NotNull VirtualComponentRenderer<?> renderer() {
return this.renderer;
}

@Override
public @NotNull String content() {
return this.renderer.fallbackString();
}

@Override
public @NotNull Builder toBuilder() {
return new BuilderImpl(this);
}

static final class BuilderImpl extends TextComponentImpl.BuilderImpl {
private final Class<?> contextType;
private final VirtualComponentRenderer<?> renderer;

BuilderImpl(final VirtualComponent other) {
super(other);
this.contextType = other.contextType();
this.renderer = other.renderer();
}

@Override
public @NotNull TextComponent build() {
return createVirtual(this.contextType, this.renderer, this.children, this.buildStyle());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2017-2024 KyoriPowered
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.kyori.adventure.text;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnknownNullability;

/**
* A holder for a value.
*
* @param <C> the context type
* @since 4.16.0
*/
public interface VirtualComponentRenderer<C> {
/**
* Gets the value by rendering using {@code context}.
*
* @param context the context
* @return the rendered value
* @since 4.16.0
*/
@UnknownNullability ComponentLike apply(final @NotNull C context);

/**
* Get a fallback value for when this component has been serialized without being rendered.
*
* <p>By default, this will be an empty string.</p>
*
* @return the fallback string
* @since 4.16.0
*/
default @NotNull String fallbackString() {
return "";
}
}
Loading