Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,41 @@
import static com.spinyowl.spinygui.core.backend.renderer.lwjgl.nanovg.util.NvgRenderUtils.withPresentedOpacity;
import static com.spinyowl.spinygui.core.backend.renderer.lwjgl.nanovg.util.NvgShapes.drawRect;
import static com.spinyowl.spinygui.core.util.NodeUtilities.visible;
import static org.lwjgl.nanovg.NanoVG.NVG_IMAGE_FLIPY;
import static org.lwjgl.nanovg.NanoVG.nvgBeginPath;
import static org.lwjgl.nanovg.NanoVG.nvgCreateImage;
import static org.lwjgl.nanovg.NanoVG.nvgFill;
import static org.lwjgl.nanovg.NanoVG.nvgFillPaint;
import static org.lwjgl.nanovg.NanoVG.nvgImagePattern;
import static org.lwjgl.nanovg.NanoVG.nvgRect;
import static org.lwjgl.nanovg.NanoVG.nvgRestore;
import static org.lwjgl.nanovg.NanoVG.nvgRoundedRect;
import static org.lwjgl.nanovg.NanoVG.nvgRoundedRectVarying;
import static org.lwjgl.nanovg.NanoVG.nvgSave;
import static org.lwjgl.system.MemoryStack.stackPush;

import com.spinyowl.spinygui.core.backend.renderer.lwjgl.nanovg.diagnostic.NvgDiagnosticCounter;
import com.spinyowl.spinygui.core.diagnostic.DiagnosticSession;
import com.spinyowl.spinygui.core.node.Element;
import com.spinyowl.spinygui.core.node.Node;
import com.spinyowl.spinygui.core.style.types.Display;
import java.util.HashMap;
import java.util.Map;
import org.joml.Vector2f;
import org.joml.Vector4f;
import org.lwjgl.nanovg.NVGPaint;
import org.lwjgl.nanovg.NanoVGGL2;
import org.lwjgl.nanovg.NanoVGGL3;
import org.lwjgl.opengl.GL30;

public class NvgElementRenderer {
private static final String IMG = "img";
private static final String SRC = "src";
private static final String TEXTURE_ID = "texture-id";
private static final String FLIP_Y = "flip-y";

private final DiagnosticSession diagnostics;
private final Map<String, Integer> imageCache = new HashMap<>();

public NvgElementRenderer() {
this(DiagnosticSession.disabled());
Expand Down Expand Up @@ -54,22 +77,129 @@ public void render(Node node, long nanovg) {
var position = element.layoutAbsolutePosition();
var size = element.size();

// render self
// Always render the element rectangle first. For <img>, this is also the fallback when the
// source cannot be resolved or loaded.
diagnostics.increment(NvgDiagnosticCounter.SAVE_CALLS);
nvgSave(nanovg);
drawRect(nanovg, position, size, backgroundColor, borderRadius);
if (IMG.equalsIgnoreCase(element.nodeName())) {
renderImage(element, nanovg, position, size, borderRadius);
}
diagnostics.increment(NvgDiagnosticCounter.RESTORE_CALLS);
nvgRestore(nanovg);
}
}

private void renderImage(
Element element, long nanovg, Vector2f position, Vector2f size, Vector4f borderRadius) {
if (size.x <= 0 || size.y <= 0) {
return;
}

int image = resolveImage(element, nanovg, size);
if (image <= 0) {
return;
}

try (var stack = stackPush()) {
NVGPaint paint = NVGPaint.malloc(stack);
nvgImagePattern(nanovg, position.x, position.y, size.x, size.y, 0f, image, 1f, paint);
nvgBeginPath(nanovg);
if (borderRadius != null && !borderRadius.equals(new Vector4f(0))) {
if (borderRadius.x == borderRadius.y
&& borderRadius.x == borderRadius.z
&& borderRadius.x == borderRadius.w) {
nvgRoundedRect(nanovg, position.x, position.y, size.x, size.y, borderRadius.x);
} else {
nvgRoundedRectVarying(
nanovg,
position.x,
position.y,
size.x,
size.y,
borderRadius.x,
borderRadius.y,
borderRadius.z,
borderRadius.w);
}
} else {
nvgRect(nanovg, position.x, position.y, size.x, size.y);
}
nvgFillPaint(nanovg, paint);
nvgFill(nanovg);
}
}

private int resolveImage(Element element, long nanovg, Vector2f size) {
String textureId = attribute(element, TEXTURE_ID);
if (textureId != null) {
return resolveExternalTexture(element, nanovg, size, textureId);
}

String src = attribute(element, SRC);
if (src == null) {
return 0;
}
return imageCache.computeIfAbsent("src:" + src, ignored -> createImage(nanovg, src));
}

private int resolveExternalTexture(
Element element, long nanovg, Vector2f size, String textureIdValue) {
final int textureId;
try {
textureId = Integer.parseUnsignedInt(textureIdValue);
} catch (NumberFormatException ignored) {
return 0;
}
if (textureId == 0) {
return 0;
}

int width = Math.max(1, Math.round(size.x));
int height = Math.max(1, Math.round(size.y));
boolean flipY = Boolean.parseBoolean(attribute(element, FLIP_Y));
String key = "texture:" + textureId + ':' + width + 'x' + height + ':' + flipY;
return imageCache.computeIfAbsent(
key, ignored -> importExternalTexture(nanovg, textureId, width, height, flipY));
}

private int createImage(long nanovg, String src) {
try (var stack = stackPush()) {
return nvgCreateImage(nanovg, stack.UTF8(src), 0);
} catch (RuntimeException ignored) {
return 0;
}
}

private int importExternalTexture(
long nanovg, int textureId, int width, int height, boolean flipY) {
int imageFlags = flipY ? NVG_IMAGE_FLIPY : 0;
try {
boolean gl3 =
(GL30.glGetInteger(GL30.GL_MAJOR_VERSION) > 3)
|| GL30.glGetInteger(GL30.GL_MAJOR_VERSION) == 3
&& GL30.glGetInteger(GL30.GL_MINOR_VERSION) >= 2;
if (gl3) {
return NanoVGGL3.nvglCreateImageFromHandle(
nanovg, textureId, width, height, imageFlags | NanoVGGL3.NVG_IMAGE_NODELETE);
}
return NanoVGGL2.nvglCreateImageFromHandle(
nanovg, textureId, width, height, imageFlags | NanoVGGL2.NVG_IMAGE_NODELETE);
} catch (RuntimeException ignored) {
return 0;
}
}

private String attribute(Element element, String name) {
String value = element.attributes().get(name);
return value == null || value.isBlank() ? null : value.trim();
}

Vector2f inlineFormattingOffset(Element element) {
Element parent = element.parent();
while (parent != null && Display.INLINE.equals(parent.resolvedStyle().display())) {
parent = parent.parent();
}
return parent == null
? new Vector2f()
: parent.layoutAbsolutePosition();
return parent == null ? new Vector2f() : parent.layoutAbsolutePosition();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.spinyowl.spinygui.core.node;

/** HTML {@code img} element. */
public class ImgElement extends EmptyElement {

public static final String IMG_TAG_NAME = "img";
public static final String SRC_ATTRIBUTE = "src";
public static final String TEXTURE_ID_ATTRIBUTE = "texture-id";
public static final String FLIP_Y_ATTRIBUTE = "flip-y";

public ImgElement() {
super(IMG_TAG_NAME);
}

/** Returns the conventional image source, or {@code null} when it is not specified. */
public String src() {
return attribute(SRC_ATTRIBUTE);
}

/**
* Returns an external renderer texture identifier, or {@code null} when it is not specified.
*
* <p>The interpretation of this value is backend-specific. The LWJGL NanoVG backend interprets
* it as an OpenGL texture handle and imports it without taking ownership of the texture.
*/
public String textureId() {
return attribute(TEXTURE_ID_ATTRIBUTE);
}

/** Whether the image should be vertically flipped by the renderer. */
public boolean flipY() {
return Boolean.parseBoolean(attribute(FLIP_Y_ATTRIBUTE));
}

private String attribute(String name) {
String value = attributes().get(name);
return value == null || value.isBlank() ? null : value.trim();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.spinyowl.spinygui.core.node.Element;
import com.spinyowl.spinygui.core.node.EmptyElement;
import com.spinyowl.spinygui.core.node.Frame;
import com.spinyowl.spinygui.core.node.ImgElement;
import com.spinyowl.spinygui.core.node.InputElement;
import com.spinyowl.spinygui.core.node.Node;
import com.spinyowl.spinygui.core.node.Text;
Expand Down Expand Up @@ -78,6 +79,8 @@ private Node createNodeFromElement(
Node node;
if ("input".equals(tagName)) {
node = new InputElement();
} else if (ImgElement.IMG_TAG_NAME.equals(tagName)) {
node = new ImgElement();
} else if ("button".equals(tagName)) {
node = new ButtonElement();
createChildNodes(element, context, node);
Expand Down Expand Up @@ -107,8 +110,6 @@ private Node createNodeFromElement(
return node;
}

// unmarshaller section

private void createChildNodes(
org.jsoup.nodes.Element element, NodeConverterContext context, Node node) {
var childNodes = element.childNodes();
Expand Down
Loading