diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9646032..88f6811 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -9,6 +9,9 @@ jetbrains_annotations = "24.0.0"
# Elementa
elementa = "619"
+# PolyUI
+polyui = "1.7.29"
+
[libraries]
# Commonmark
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
@@ -22,4 +25,7 @@ tagsoup = { module = "org.ccil.cowan.tagsoup:tagsoup", version.ref = "tagsoup" }
jetbrains_annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains_annotations" }
# Elementa
-elementa = { module = "gg.essential:elementa-1.8.9-forge", version.ref = "elementa" }
\ No newline at end of file
+elementa = { module = "gg.essential:elementa-1.8.9-forge", version.ref = "elementa" }
+
+# PolyUI
+polyui = { module = "org.polyfrost:polyui", version.ref = "polyui" }
\ No newline at end of file
diff --git a/polyui/build.gradle.kts b/polyui/build.gradle.kts
new file mode 100644
index 0000000..8f33374
--- /dev/null
+++ b/polyui/build.gradle.kts
@@ -0,0 +1,63 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+plugins {
+ kotlin("jvm") version "2.0.21" // Same version as PolyUI
+}
+
+repositories {
+ maven("https://repo.polyfrost.org/releases")
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ }
+}
+
+dependencies {
+ implementation(libs.polyui)
+ implementation(libs.commonmark.ext.striketrough)
+ implementation(libs.commonmark.ext.tables)
+
+ testImplementation(kotlin("test"))
+
+ // Taken from PolyUI for testing since they don't publish any rendering implementations?
+ // version of LWJGL to use. Recommended to be latest.
+ val lwjglVersion = "3.3.3"
+
+ // list of modules that this implementation needs to work.
+ val lwjglModules = listOf("nanovg", "opengl", "stb", "glfw", null)
+
+ // list of platforms that this implementation will support.
+ val nativePlatforms = listOf("windows", "linux", "macos", "macos-arm64")
+
+ for (module in lwjglModules) {
+ val dep = if(module == null) "org.lwjgl:lwjgl:$lwjglVersion" else "org.lwjgl:lwjgl-$module:$lwjglVersion"
+ testImplementation(dep)
+ for (platform in nativePlatforms) {
+ testRuntimeOnly("$dep:natives-$platform")
+ }
+ }
+ testImplementation("org.apache.logging.log4j:log4j-api:2.24.1")
+}
+
+tasks.test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/MarkdownComponent.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/MarkdownComponent.kt
new file mode 100644
index 0000000..b49e5a1
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/MarkdownComponent.kt
@@ -0,0 +1,127 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package dev.dediamondpro.minemark.polyui
+
+import dev.dediamondpro.minemark.MineMarkCore
+import dev.dediamondpro.minemark.MineMarkCoreBuilder
+import dev.dediamondpro.minemark.elements.Elements
+import dev.dediamondpro.minemark.elements.MineMarkElement
+import dev.dediamondpro.minemark.polyui.elements.*
+import dev.dediamondpro.minemark.utils.MouseButton
+import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension
+import org.commonmark.ext.gfm.tables.TablesExtension
+import org.polyfrost.polyui.color.Colors
+import org.polyfrost.polyui.component.Component
+import org.polyfrost.polyui.component.Drawable
+import org.polyfrost.polyui.component.extensions.events
+import org.polyfrost.polyui.event.Event
+import org.polyfrost.polyui.unit.Align
+import org.polyfrost.polyui.unit.AlignDefault
+import org.polyfrost.polyui.unit.Vec2
+import java.io.Reader
+
+class MarkdownComponent(
+ markdown: MineMarkElement,
+ vararg children: Component? = arrayOf(),
+ at: Vec2 = Vec2.ZERO,
+ alignment: Align = AlignDefault,
+ size: Vec2 = Vec2.ZERO,
+ visibleSize: Vec2 = Vec2.ZERO,
+ palette: Colors.Palette? = null,
+ focusable: Boolean = false,
+) : Drawable(children = children, at, alignment, size, visibleSize, palette, focusable) {
+ constructor(
+ markdown: String,
+ vararg children: Component? = arrayOf(),
+ at: Vec2 = Vec2.ZERO,
+ alignment: Align = AlignDefault,
+ size: Vec2 = Vec2.ZERO,
+ visibleSize: Vec2 = Vec2.ZERO,
+ palette: Colors.Palette? = null,
+ focusable: Boolean = false,
+ style: MarkdownStyle = MarkdownStyle(),
+ core: MineMarkCore = defaultCore,
+ ) : this(core.parse(style, markdown), children = children, at, alignment, size, visibleSize, palette, focusable)
+
+ constructor(
+ markdown: Reader,
+ vararg children: Component? = arrayOf(),
+ at: Vec2 = Vec2.ZERO,
+ alignment: Align = AlignDefault,
+ size: Vec2 = Vec2.ZERO,
+ visibleSize: Vec2 = Vec2.ZERO,
+ palette: Colors.Palette? = null,
+ focusable: Boolean = false,
+ style: MarkdownStyle = MarkdownStyle(),
+ core: MineMarkCore = defaultCore,
+ ) : this(core.parse(style, markdown), children = children, at, alignment, size, visibleSize, palette, focusable)
+
+ val parsedMarkdown = markdown.apply {
+ addLayoutCallback(this@MarkdownComponent::layoutCallback)
+ }
+
+ init {
+ events {
+ Event.Mouse.Clicked(0).then {
+ parsedMarkdown.onMouseClicked(x, y, MouseButton.LEFT, it.x, it.y)
+ }
+ Event.Mouse.Clicked(1).then {
+ parsedMarkdown.onMouseClicked(x, y, MouseButton.RIGHT, it.x, it.y)
+ }
+ Event.Mouse.Clicked(2).then {
+ parsedMarkdown.onMouseClicked(x, y, MouseButton.MIDDLE, it.x, it.y)
+ }
+ }
+ }
+
+ override fun preRender(delta: Long) {
+ super.preRender(delta)
+ parsedMarkdown.beforeDraw(x, y, width, polyUI.mouseX, polyUI.mouseY, this)
+ }
+
+ override fun render() {
+ parsedMarkdown.draw(x, y, width, polyUI.mouseX, polyUI.mouseY, this)
+ if (parsedMarkdown.needsLayoutRegeneration(width)) {
+ // If our elements have decided the layout needs to change, we need to redraw
+ needsRedraw = true
+ }
+ }
+
+ private fun layoutCallback(newHeight: Float) {
+ height = newHeight
+ }
+
+ companion object {
+ private val defaultCore = MineMarkCore.builder()
+ .addExtension(StrikethroughExtension.create())
+ .addExtension(TablesExtension.create())
+ .addPolyUIExtensions()
+ .build()
+ }
+}
+
+fun MineMarkCoreBuilder.addPolyUIExtensions(): MineMarkCoreBuilder {
+ return this.setTextElement(::MarkdownTextElement)
+ .addElement(Elements.IMAGE, ::MarkdownImageElement)
+ .addElement(Elements.HEADING, ::MarkdownHeadingElement)
+ .addElement(Elements.HORIZONTAL_RULE, ::MarkdownHorizontalRuleElement)
+ .addElement(Elements.CODE_BLOCK, ::MarkdownCodeBlockElement)
+ .addElement(Elements.BLOCKQUOTE, ::MarkdownBlockquoteElement)
+ .addElement(Elements.LIST_ELEMENT, ::MarkdownListElement)
+ .addElement(Elements.TABLE_CELL, ::MarkdownTableCellElement)
+}
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/MarkdownImageProvider.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/MarkdownImageProvider.kt
new file mode 100644
index 0000000..0376c9b
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/MarkdownImageProvider.kt
@@ -0,0 +1,32 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package dev.dediamondpro.minemark.polyui
+
+import dev.dediamondpro.minemark.providers.ImageProvider
+import org.polyfrost.polyui.data.PolyImage
+import java.util.function.Consumer
+
+object MarkdownImageProvider : ImageProvider {
+ override fun getImage(
+ src: String,
+ dimensionCallback: Consumer,
+ imageCallback: Consumer,
+ ) {
+ imageCallback.accept(PolyImage(src))
+ }
+}
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/MarkdownStyle.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/MarkdownStyle.kt
new file mode 100644
index 0000000..6aef97e
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/MarkdownStyle.kt
@@ -0,0 +1,93 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package dev.dediamondpro.minemark.polyui
+
+import dev.dediamondpro.minemark.providers.DefaultBrowserProvider
+import dev.dediamondpro.minemark.style.*
+import org.polyfrost.polyui.PolyUI
+import org.polyfrost.polyui.component.Drawable
+import org.polyfrost.polyui.data.Font
+import java.awt.Color
+
+class MarkdownStyle
+@JvmOverloads
+constructor(
+ private val textStyle: MarkdownTextStyle = MarkdownTextStyle(),
+ private val paragraphStyle: ParagraphStyleConfig = ParagraphStyleConfig(6f),
+ private val linkStyle: LinkStyleConfig = LinkStyleConfig(Color(65, 105, 225), DefaultBrowserProvider.INSTANCE),
+ private val headerStyle: HeadingStyleConfig =
+ HeadingStyleConfig(
+ HeadingLevelStyleConfig(32f, 12f, true, LINE_COLOR, 2f, 5f),
+ HeadingLevelStyleConfig(24f, 10f, true, LINE_COLOR, 2f, 5f),
+ HeadingLevelStyleConfig(19f, 8f),
+ HeadingLevelStyleConfig(16f, 6f),
+ HeadingLevelStyleConfig(13f, 4f),
+ HeadingLevelStyleConfig(13f, 4f),
+ ),
+ private val horizontalRuleStyle: HorizontalRuleStyleConfig = HorizontalRuleStyleConfig(2f, 4f, LINE_COLOR),
+ private val imageStyle: ImageStyleConfig = ImageStyleConfig(MarkdownImageProvider),
+ private val listStyle: ListStyleConfig = ListStyleConfig(32f, 6f),
+ private val blockquoteBlockStyle: BlockquoteStyleConfig = BlockquoteStyleConfig(6f, 4f, 2f, 10f, LINE_COLOR),
+ private val codeBlockStyle: CodeBlockStyle = CodeBlockStyle(),
+ private val tableStyle: TableStyleConfig = TableStyleConfig(
+ 6f, 4f, 1f, LINE_COLOR, Color(0, 0, 0, 150), Color(0, 0, 0, 0)
+ ),
+) : Style {
+ override fun getTextStyle(): MarkdownTextStyle = textStyle
+
+ override fun getParagraphStyle(): ParagraphStyleConfig = paragraphStyle
+
+ override fun getLinkStyle(): LinkStyleConfig = linkStyle
+
+ override fun getHeadingStyle(): HeadingStyleConfig = headerStyle
+
+ override fun getHorizontalRuleStyle(): HorizontalRuleStyleConfig = horizontalRuleStyle
+
+ override fun getImageStyle(): ImageStyleConfig = imageStyle
+
+ override fun getListStyle(): ListStyleConfig = listStyle
+
+ override fun getBlockquoteStyle(): BlockquoteStyleConfig = blockquoteBlockStyle
+
+ override fun getCodeBlockStyle(): CodeBlockStyle = codeBlockStyle
+
+ override fun getTableStyle(): TableStyleConfig = tableStyle
+
+ companion object {
+ internal val LINE_COLOR = Color(80, 80, 80)
+ }
+}
+
+class MarkdownTextStyle(
+ val normalFont: Font = PolyUI.defaultFonts.medium,
+ val boldFont: Font = PolyUI.defaultFonts.bold,
+ val italicNormalFont: Font = PolyUI.defaultFonts.mediumItalic,
+ val italicBoldFont: Font = PolyUI.defaultFonts.boldItalic,
+ defaultFontSize: Float = 16f,
+ defaultTextColor: Color = Color.WHITE,
+ padding: Float = (normalFont.lineSpacing - 1f) * defaultFontSize / 2f,
+) : TextStyleConfig(defaultFontSize, defaultTextColor, padding)
+
+class CodeBlockStyle(
+ val codeFont: Font = PolyUI.monospaceFont,
+ inlinePaddingLeftRight: Float = 2f,
+ inlinePaddingTopBottom: Float = 1f,
+ blockOutsidePadding: Float = 6f,
+ blockInsidePadding: Float = 6f,
+ color: Color = MarkdownStyle.LINE_COLOR,
+) : CodeBlockStyleConfig(inlinePaddingLeftRight, inlinePaddingTopBottom, blockOutsidePadding, blockInsidePadding, color)
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownBlockquoteElement.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownBlockquoteElement.kt
new file mode 100644
index 0000000..946ce3a
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownBlockquoteElement.kt
@@ -0,0 +1,51 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+
+package dev.dediamondpro.minemark.polyui.elements
+
+import dev.dediamondpro.minemark.LayoutStyle
+import dev.dediamondpro.minemark.elements.Element
+import dev.dediamondpro.minemark.elements.impl.BlockQuoteElement
+import org.polyfrost.polyui.color.PolyColor
+import dev.dediamondpro.minemark.polyui.MarkdownStyle
+import org.polyfrost.polyui.color.toPolyColor
+import org.polyfrost.polyui.component.Drawable
+import org.polyfrost.polyui.renderer.Renderer
+import org.xml.sax.Attributes
+import java.awt.Color
+
+class MarkdownBlockquoteElement(
+ style: MarkdownStyle,
+ layoutStyle: LayoutStyle,
+ parent: Element?,
+ qName: String,
+ attributes: Attributes?,
+) : BlockQuoteElement(style, layoutStyle, parent, qName, attributes) {
+ private var polyColor: PolyColor = style.blockquoteStyle.blockColor.toPolyColor()
+
+ override fun drawBlock(
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ color: Color,
+ drawable: Drawable,
+ ) {
+ drawable.renderer.rect(x, y, width, height, polyColor)
+ }
+}
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownCodeBlockElement.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownCodeBlockElement.kt
new file mode 100644
index 0000000..78bb188
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownCodeBlockElement.kt
@@ -0,0 +1,51 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+
+package dev.dediamondpro.minemark.polyui.elements
+
+import dev.dediamondpro.minemark.LayoutStyle
+import dev.dediamondpro.minemark.elements.Element
+import dev.dediamondpro.minemark.elements.impl.CodeBlockElement
+import org.polyfrost.polyui.color.PolyColor
+import dev.dediamondpro.minemark.polyui.MarkdownStyle
+import org.polyfrost.polyui.color.toPolyColor
+import org.polyfrost.polyui.component.Drawable
+import org.polyfrost.polyui.renderer.Renderer
+import org.xml.sax.Attributes
+import java.awt.Color
+
+class MarkdownCodeBlockElement(
+ style: MarkdownStyle,
+ layoutStyle: LayoutStyle,
+ parent: Element?,
+ qName: String,
+ attributes: Attributes?,
+) : CodeBlockElement(style, layoutStyle, parent, qName, attributes) {
+ private var polyColor: PolyColor = style.codeBlockStyle.color.toPolyColor()
+
+ override fun drawBlock(
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ color: Color,
+ drawable: Drawable,
+ ) {
+ drawable.renderer.rect(x, y, width, height, polyColor, radius = 4f)
+ }
+}
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownHeadingElement.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownHeadingElement.kt
new file mode 100644
index 0000000..5de6b51
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownHeadingElement.kt
@@ -0,0 +1,52 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+
+package dev.dediamondpro.minemark.polyui.elements
+
+import dev.dediamondpro.minemark.LayoutStyle
+import dev.dediamondpro.minemark.elements.Element
+import dev.dediamondpro.minemark.elements.impl.HeadingElement
+import org.polyfrost.polyui.color.PolyColor
+import dev.dediamondpro.minemark.polyui.MarkdownStyle
+import org.polyfrost.polyui.color.toPolyColor
+import org.polyfrost.polyui.component.Drawable
+import org.polyfrost.polyui.renderer.Renderer
+import org.xml.sax.Attributes
+import java.awt.Color
+
+class MarkdownHeadingElement(
+ style: MarkdownStyle,
+ layoutStyle: LayoutStyle,
+ parent: Element?,
+ qName: String,
+ attributes: Attributes?,
+) : HeadingElement(style, layoutStyle, parent, qName, attributes) {
+ // If this is a heading without divider it is possible the color is null
+ private var polyColor: PolyColor? = headingStyle.dividerColor?.toPolyColor()
+
+ override fun drawDivider(
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ color: Color,
+ drawable: Drawable,
+ ) {
+ drawable.renderer.rect(x, y, width, height, polyColor!!)
+ }
+}
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownHorizontalRuleElement.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownHorizontalRuleElement.kt
new file mode 100644
index 0000000..d9c67a1
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownHorizontalRuleElement.kt
@@ -0,0 +1,51 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+
+package dev.dediamondpro.minemark.polyui.elements
+
+import dev.dediamondpro.minemark.LayoutStyle
+import dev.dediamondpro.minemark.elements.Element
+import dev.dediamondpro.minemark.elements.impl.HorizontalRuleElement
+import org.polyfrost.polyui.color.PolyColor
+import dev.dediamondpro.minemark.polyui.MarkdownStyle
+import org.polyfrost.polyui.color.toPolyColor
+import org.polyfrost.polyui.component.Drawable
+import org.polyfrost.polyui.renderer.Renderer
+import org.xml.sax.Attributes
+import java.awt.Color
+
+class MarkdownHorizontalRuleElement(
+ style: MarkdownStyle,
+ layoutStyle: LayoutStyle,
+ parent: Element?,
+ qName: String,
+ attributes: Attributes?,
+) : HorizontalRuleElement(style, layoutStyle, parent, qName, attributes) {
+ private var polyColor: PolyColor = style.horizontalRuleStyle.color.toPolyColor()
+
+ override fun drawLine(
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ color: Color,
+ drawable: Drawable,
+ ) {
+ drawable.renderer.rect(x, y, width, height, polyColor)
+ }
+}
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownImageElement.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownImageElement.kt
new file mode 100644
index 0000000..0f4a31c
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownImageElement.kt
@@ -0,0 +1,89 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+
+package dev.dediamondpro.minemark.polyui.elements
+
+import dev.dediamondpro.minemark.LayoutStyle
+import dev.dediamondpro.minemark.elements.Element
+import dev.dediamondpro.minemark.elements.impl.ImageElement
+import dev.dediamondpro.minemark.polyui.MarkdownComponent
+import dev.dediamondpro.minemark.providers.ImageProvider
+import dev.dediamondpro.minemark.polyui.MarkdownStyle
+import org.polyfrost.polyui.component.Drawable
+import org.polyfrost.polyui.data.PolyImage
+import org.polyfrost.polyui.renderer.Renderer
+import org.polyfrost.polyui.unit.Vec2
+import org.xml.sax.Attributes
+
+class MarkdownImageElement(
+ style: MarkdownStyle,
+ layoutStyle: LayoutStyle,
+ parent: Element?,
+ qName: String,
+ attributes: Attributes?,
+) : ImageElement(style, layoutStyle, parent, qName, attributes) {
+ private var loadFailed: Boolean = false
+
+ override fun beforeDrawInternal(
+ xOffset: Float,
+ yOffset: Float,
+ mouseX: Float,
+ mouseY: Float,
+ drawable: Drawable
+ ) {
+ super.beforeDrawInternal(xOffset, yOffset, mouseX, mouseY, drawable)
+ if (image.size.isZero && imageWidth == -1f && imageHeight == -1f && !loadFailed) {
+ // Image is most likely not initialized
+ try {
+ drawable.renderer.initImage(image, Vec2(width, height))
+ } catch (_: Exception) {
+ // It is possible initialization fails, if this happens we will just draw nothing
+ loadFailed = true
+ return
+ }
+ if (image.size.isZero) {
+ drawable.needsRedraw = true // Keep polling, image might be loading asynchronously
+ }
+ }
+ if (!image.size.isZero && imageWidth == -1f && imageHeight == -1f && !loadFailed) {
+ onDimensionsReceived(ImageProvider.Dimension(image.size.x, image.size.y))
+ return // We have to regenerate layout, this also calls for a redraw
+ }
+ }
+
+ override fun drawImage(
+ image: PolyImage,
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ drawable: Drawable,
+ ) {
+ if (width == 0f && height == 0f || imageWidth == -1f && imageHeight == -1f || loadFailed) {
+ return // Nothing to draw, might not be initialized
+ }
+ drawable.renderer.image(image, x, y, width, height)
+ }
+
+ override fun onImageReceived(image: PolyImage) {
+ if (!image.size.isZero) {
+ onDimensionsReceived(ImageProvider.Dimension(image.size.x, image.size.y))
+ }
+ super.onImageReceived(image)
+ }
+}
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownListElement.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownListElement.kt
new file mode 100644
index 0000000..8931592
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownListElement.kt
@@ -0,0 +1,84 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+
+package dev.dediamondpro.minemark.polyui.elements
+
+import dev.dediamondpro.minemark.LayoutData
+import dev.dediamondpro.minemark.LayoutStyle
+import dev.dediamondpro.minemark.elements.Element
+import dev.dediamondpro.minemark.elements.impl.list.ListElement
+import dev.dediamondpro.minemark.elements.impl.list.ListHolderElement
+import org.polyfrost.polyui.color.PolyColor
+import dev.dediamondpro.minemark.polyui.MarkdownStyle
+import org.polyfrost.polyui.color.toPolyColor
+import org.polyfrost.polyui.component.Drawable
+import org.polyfrost.polyui.renderer.Renderer
+import org.polyfrost.polyui.unit.Vec2
+import org.xml.sax.Attributes
+
+class MarkdownListElement(
+ style: MarkdownStyle,
+ layoutStyle: LayoutStyle,
+ parent: Element?,
+ qName: String,
+ attributes: Attributes?,
+) : ListElement(style, layoutStyle, parent, qName, attributes) {
+ private val markerStr: String =
+ when (listType) {
+ ListHolderElement.ListType.ORDERED -> "${elementIndex + 1}. "
+ ListHolderElement.ListType.UNORDERED -> "- "
+ else -> "- "
+ }
+ private var markerBounds: Vec2? = null
+
+ override fun drawMarker(
+ x: Float,
+ y: Float,
+ drawable: Drawable,
+ ) {
+ drawable.renderer.text(
+ style.textStyle.normalFont,
+ x,
+ y,
+ markerStr,
+ style.textStyle.defaultTextColor.toPolyColor(),
+ style.textStyle.defaultFontSize,
+ )
+ }
+
+ private fun getMarkerBounds(renderer: Renderer): Vec2 {
+ if (markerBounds == null) {
+ markerBounds = renderer.textBounds(style.textStyle.normalFont, markerStr, style.textStyle.defaultFontSize)
+ }
+ return markerBounds!!
+ }
+
+ override fun getListMarkerWidth(
+ layoutData: LayoutData,
+ drawable: Drawable,
+ ): Float {
+ return getMarkerBounds(drawable.renderer).x
+ }
+
+ override fun getMarkerHeight(
+ layoutData: LayoutData,
+ drawable: Drawable,
+ ): Float {
+ return getMarkerBounds(drawable.renderer).y
+ }
+}
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownTableCellElement.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownTableCellElement.kt
new file mode 100644
index 0000000..2f26b44
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownTableCellElement.kt
@@ -0,0 +1,65 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package dev.dediamondpro.minemark.polyui.elements
+
+import dev.dediamondpro.minemark.LayoutStyle
+import dev.dediamondpro.minemark.elements.Element
+import dev.dediamondpro.minemark.elements.impl.table.TableCellElement
+import org.polyfrost.polyui.color.PolyColor
+import dev.dediamondpro.minemark.polyui.MarkdownStyle
+import org.polyfrost.polyui.color.toPolyColor
+import org.polyfrost.polyui.component.Drawable
+import org.polyfrost.polyui.renderer.Renderer
+import org.xml.sax.Attributes
+import java.awt.Color
+
+class MarkdownTableCellElement(
+ style: MarkdownStyle,
+ layoutStyle: LayoutStyle,
+ parent: Element?,
+ qName: String,
+ attributes: Attributes?,
+) : TableCellElement(style, layoutStyle, parent, qName, attributes) {
+ private lateinit var fillPolyColor: PolyColor
+ private val borderPolyColor = style.tableStyle.borderColor.toPolyColor()
+
+ override fun drawCellBackground(
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ color: Color,
+ drawable: Drawable
+ ) {
+ if (!this::fillPolyColor.isInitialized) {
+ fillPolyColor = color.toPolyColor()
+ }
+ drawable.renderer.rect(x, y, width, height, fillPolyColor)
+ }
+
+ override fun drawBorderLine(
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ color: Color,
+ drawable: Drawable
+ ) {
+ drawable.renderer.rect(x, y, width, height, borderPolyColor)
+ }
+}
diff --git a/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownTextElement.kt b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownTextElement.kt
new file mode 100644
index 0000000..5687606
--- /dev/null
+++ b/polyui/src/main/kotlin/dev/dediamondpro/minemark/polyui/elements/MarkdownTextElement.kt
@@ -0,0 +1,143 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+
+package dev.dediamondpro.minemark.polyui.elements
+
+import dev.dediamondpro.minemark.LayoutData.MarkDownElementPosition
+import dev.dediamondpro.minemark.LayoutStyle
+import dev.dediamondpro.minemark.elements.Element
+import dev.dediamondpro.minemark.elements.impl.TextElement
+import dev.dediamondpro.minemark.polyui.MarkdownStyle
+import org.polyfrost.polyui.color.PolyColor
+import org.polyfrost.polyui.color.toPolyColor
+import org.polyfrost.polyui.component.Drawable
+import org.polyfrost.polyui.component.extensions.events
+import org.polyfrost.polyui.event.Event
+import org.xml.sax.Attributes
+import java.awt.Color
+import kotlin.math.floor
+
+class MarkdownTextElement(
+ text: String,
+ style: MarkdownStyle,
+ layoutStyle: LayoutStyle,
+ parent: Element?,
+ qName: String,
+ attributes: Attributes?,
+) : TextElement(text, style, layoutStyle, parent, qName, attributes) {
+ private val textColor: PolyColor = layoutStyle.textColor.toPolyColor()
+ private val codeBlockColor: PolyColor = style.codeBlockStyle.color.toPolyColor()
+ private val font =
+ when {
+ layoutStyle.isPartOfCodeBlock -> style.codeBlockStyle.codeFont
+ layoutStyle.isBold && layoutStyle.isItalic -> style.textStyle.italicBoldFont
+ layoutStyle.isBold -> style.textStyle.boldFont
+ layoutStyle.isItalic -> style.textStyle.italicNormalFont
+ else -> style.textStyle.normalFont
+ }
+ private var lineHeight = -1f
+ private var registeredEvent = false
+ private var wasHovered = false
+
+ private fun registerEvent(drawable: Drawable) {
+ if (registeredEvent) return
+ registeredEvent = true
+
+ if (!layoutStyle.isPartOfLink) return
+ drawable.events {
+ Event.Mouse.Moved.then {
+ var hovered = false
+ for (position in lines.keys) {
+ if (position.isInside(drawable.polyUI.mouseX, drawable.polyUI.mouseY)) {
+ hovered = true
+ break
+ }
+ }
+ if (hovered != wasHovered) {
+ wasHovered = hovered
+ drawable.needsRedraw = true
+ }
+ }
+ }
+ }
+
+ override fun beforeDrawInternal(
+ xOffset: Float,
+ yOffset: Float,
+ mouseX: Float,
+ mouseY: Float,
+ drawable: Drawable
+ ) {
+ registerEvent(drawable)
+ super.beforeDrawInternal(xOffset, yOffset, mouseX, mouseY, drawable)
+ }
+
+ override fun drawText(
+ text: String,
+ x: Float,
+ y: Float,
+ fontSize: Float,
+ color: Color,
+ hovered: Boolean,
+ position: MarkDownElementPosition,
+ drawable: Drawable,
+ ) {
+ if (layoutStyle.isUnderlined || layoutStyle.isPartOfLink && hovered) {
+ drawable.renderer.rect(x, position.bottomY, position.width, floor(layoutStyle.fontSize / 8), textColor)
+ }
+ if (layoutStyle.isStrikethrough) {
+ drawable.renderer.rect(
+ x,
+ y + position.height / 2f - fontSize / 8f,
+ position.width,
+ floor(layoutStyle.fontSize / 8),
+ textColor,
+ )
+ }
+ drawable.renderer.text(font, x, y, text, textColor, fontSize)
+ }
+
+ override fun drawInlineCodeBlock(
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ color: Color,
+ drawable: Drawable,
+ ) {
+ drawable.renderer.rect(x, y, width, height, codeBlockColor, radius = 2f)
+ }
+
+ override fun getTextWidth(
+ text: String,
+ fontSize: Float,
+ drawable: Drawable,
+ ): Float {
+ return drawable.renderer.textBounds(font, text, fontSize).x
+ }
+
+ override fun getBaselineHeight(
+ fontSize: Float,
+ drawable: Drawable,
+ ): Float {
+ if (lineHeight == -1f) {
+ lineHeight = drawable.renderer.textBounds(font, text, fontSize).y
+ }
+ return lineHeight
+ }
+}
diff --git a/polyui/src/test/kotlin/GLFWWindow.kt b/polyui/src/test/kotlin/GLFWWindow.kt
new file mode 100644
index 0000000..2bb1452
--- /dev/null
+++ b/polyui/src/test/kotlin/GLFWWindow.kt
@@ -0,0 +1,412 @@
+/*
+ * This file is part of PolyUI
+ * PolyUI - Fast and lightweight UI framework
+ * Copyright (C) 2023-2024 Polyfrost and its contributors.
+ *
+ *
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * PolyUI is licensed under the terms of version 3 of the GNU Lesser
+ * General Public License as published by the Free Software Foundation,
+ * AND the simple request that you adequately accredit us if you use PolyUI.
+ * See details here .
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License. If not, see .
+ */
+
+package org.polyfrost.polyui.renderer.impl
+
+import org.apache.logging.log4j.LogManager
+import org.lwjgl.glfw.Callbacks
+import org.lwjgl.glfw.GLFW.*
+import org.lwjgl.glfw.GLFWDropCallback
+import org.lwjgl.glfw.GLFWErrorCallback
+import org.lwjgl.glfw.GLFWImage
+import org.lwjgl.opengl.GL.createCapabilities
+import org.lwjgl.opengl.GL.setCapabilities
+import org.lwjgl.opengl.GL20C.*
+import org.lwjgl.stb.STBImage
+import org.lwjgl.system.*
+import org.lwjgl.system.macosx.ObjCRuntime
+import org.polyfrost.polyui.PolyUI
+import org.polyfrost.polyui.data.Cursor
+import org.polyfrost.polyui.input.KeyModifiers
+import org.polyfrost.polyui.input.Keys
+import org.polyfrost.polyui.renderer.Window
+import org.polyfrost.polyui.unit.Vec2
+import org.polyfrost.polyui.utils.getResourceStream
+import org.polyfrost.polyui.utils.simplifyRatio
+import org.polyfrost.polyui.utils.toByteArray
+import org.polyfrost.polyui.utils.toDirectByteBuffer
+import java.nio.file.Paths
+import kotlin.math.max
+
+/**
+ * reference implementation of a PolyUI window using GLFW, which registers an OpenGL context, supporting all PolyUI API features.
+ *
+ * For it to work across on macOS, *forwards compatibility is enabled.* This means that only Core profiles are supported, so **only use GL classes with the `C` suffix.**
+ *
+ * On macOS, this class is equipped with a workaround to allow it to run without `-XstartOnMainThread`. It can be disabled with `-Dpolyui.glfwnomacfix`, or setting [enableMacOSFix].
+ *
+ * @param gl2 if true, the window will be registered with OpenGL [2.0C+][org.lwjgl.opengl.GL20C], else, it will use OpenGL [3.2C+][org.lwjgl.opengl.GL32C].
+ */
+class GLFWWindow @JvmOverloads constructor(
+ title: String,
+ width: Int,
+ height: Int,
+ gl2: Boolean = false,
+ resizeable: Boolean = true,
+ decorated: Boolean = true,
+) : Window(width, height) {
+ private val LOGGER = LogManager.getLogger("PolyUI/GLFW")
+ override var height: Int
+ get() = super.height
+ set(value) {
+ val h = IntArray(1)
+ glfwGetFramebufferSize(handle, null, h)
+ offset = h[0] - value
+ super.height = value
+ }
+
+ /**
+ * This value is a fix for OpenGL as it draws from the bottom left, not the bottom right.
+ *
+ * It is calculated by doing window size - framebuffer size, and is used by glViewport.
+ */
+ private var offset = 0
+ val handle: Long
+ var fpsCap: Double = 0.0
+ set(value) {
+ field = if (value == 0.0) 0.0 else 1.0 / value.toInt()
+ }
+
+ var title = title
+ set(new) {
+ field = new
+ glfwSetWindowTitle(handle, new)
+ }
+
+ var enableMacOSFix = System.getProperty("polyui.glfwnomacfix", "true").toBoolean()
+
+ init {
+ if (Platform.get() == Platform.MACOSX && enableMacOSFix) {
+ PolyUI.timed(true, "macOS detected: checking isMainThread()... (disable with -Dpolyui.glfwnomacfix)") {
+ try {
+ // kotlin copy of org.lwjgl.glfw.EventLoop.isMainThread()
+ val msgSend = ObjCRuntime.getLibrary().getFunctionAddress("objc_msgSend")
+ val currentThread = JNI.invokePPP(ObjCRuntime.objc_getClass("NSThread"), ObjCRuntime.sel_getUid("currentThread"), msgSend)
+ val isMainThread = JNI.invokePPZ(currentThread, ObjCRuntime.sel_getUid("isMainThread"), msgSend)
+ if (!isMainThread) {
+ LOGGER.warn("VM option -XstartOnMainThread is required on macOS. glfw_async has been set to avoid crashing.")
+ Configuration.GLFW_LIBRARY_NAME.set("glfw_async")
+ }
+ } catch (e: Exception) {
+ LOGGER.error("Failed to check if isMainThread, may crash!", e)
+ }
+ }
+ }
+
+ val codes = APIUtil.apiClassTokens({ _, value -> value in 0x10001..0x1ffff }, null, org.lwjgl.glfw.GLFW::class.java)
+ glfwSetErrorCallback { code, desc ->
+ val stack = Thread.currentThread().stackTrace.drop(4).joinToString("\n\t at ")
+ LOGGER.error("${codes[code]} ($code): ${GLFWErrorCallback.getDescription(desc)}\nStack: $stack")
+ }
+ if (!glfwInit()) throw RuntimeException("Failed to init GLFW")
+
+ if (gl2) {
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2)
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0)
+ } else {
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3)
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2)
+ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE)
+ glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE)
+ }
+
+ if (!resizeable) glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE)
+ if (!decorated) glfwWindowHint(GLFW_DECORATED, GLFW_FALSE)
+
+ glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE)
+
+ handle = glfwCreateWindow(width, height, title, MemoryUtil.NULL, MemoryUtil.NULL)
+ if (handle == MemoryUtil.NULL) {
+ glfwTerminate()
+ throw RuntimeException("Failed to create the window.")
+ }
+
+ glfwMakeContextCurrent(handle)
+ createCapabilities()
+ println("System Information:")
+ println("\tGPU: ${glGetString(GL_RENDERER)}; ${glGetString(GL_VENDOR)}")
+ println("\tDriver version: ${glGetString(GL_VERSION)}")
+ println("\tOS: ${System.getProperty("os.name")} v${System.getProperty("os.version")}; ${System.getProperty("os.arch")}")
+ println("\tJava version: ${System.getProperty("java.version")}; ${System.getProperty("java.vm.name")} from ${System.getProperty("java.vendor")} (${System.getProperty("java.vendor.url")})")
+ }
+
+ fun createCallbacks(polyUI: PolyUI) {
+ // Add some callbacks for window resizing and content scale
+ glfwSetFramebufferSizeCallback(handle) { _, width, height ->
+ this.width = width
+ this.height = height
+ polyUI.resize(width.toFloat() / pixelRatio, height.toFloat() / pixelRatio)
+ }
+
+ glfwSetWindowContentScaleCallback(handle) { _, xScale, yScale ->
+ val pixelRatio = max(xScale, yScale)
+ if (polyUI.settings.debug) LOGGER.info("Pixel ratio: $pixelRatio")
+ this.pixelRatio = pixelRatio
+ polyUI.resize(width.toFloat() / pixelRatio, height.toFloat() / pixelRatio)
+ }
+
+ glfwSetMouseButtonCallback(handle) { _, button, action, _ ->
+ if (action == GLFW_PRESS) {
+ polyUI.inputManager.mousePressed(button)
+ } else if (action == GLFW_RELEASE) {
+ polyUI.inputManager.mouseReleased(button)
+ }
+ }
+
+ glfwSetCursorPosCallback(handle) { _, x, y ->
+ polyUI.inputManager.mouseMoved(x.toFloat(), y.toFloat())
+ }
+
+ glfwSetKeyCallback(handle) { _, keyCode, _, action, mods ->
+ if (action == GLFW_REPEAT) return@glfwSetKeyCallback
+ if (keyCode < 255 && mods > 1 && action == GLFW_PRESS) {
+ // accept modded chars, as glfwSetCharModsCallback is deprecated and doesn't work with control
+ polyUI.inputManager.keyTyped((keyCode + 32).toChar())
+ }
+ // p.s. I have performance tested this; and it is very fast (doesn't even show up on profiler). kotlin is good at int ranges lol
+ if (keyCode in 255..348) {
+ if (keyCode < 340) {
+ val key = when (keyCode) {
+ GLFW_KEY_F1 -> Keys.F1
+ GLFW_KEY_F2 -> Keys.F2
+ GLFW_KEY_F3 -> Keys.F3
+ GLFW_KEY_F4 -> Keys.F4
+ GLFW_KEY_F5 -> Keys.F5
+ GLFW_KEY_F6 -> Keys.F6
+ GLFW_KEY_F7 -> Keys.F7
+ GLFW_KEY_F8 -> Keys.F8
+ GLFW_KEY_F9 -> Keys.F9
+ GLFW_KEY_F10 -> Keys.F10
+ GLFW_KEY_F11 -> Keys.F11
+ GLFW_KEY_F12 -> Keys.F12
+
+ GLFW_KEY_ESCAPE -> Keys.ESCAPE
+
+ GLFW_KEY_ENTER -> Keys.ENTER
+ GLFW_KEY_TAB -> Keys.TAB
+ GLFW_KEY_BACKSPACE -> Keys.BACKSPACE
+ GLFW_KEY_INSERT -> Keys.INSERT
+ GLFW_KEY_DELETE -> Keys.DELETE
+ GLFW_KEY_PAGE_UP -> Keys.PAGE_UP
+ GLFW_KEY_PAGE_DOWN -> Keys.PAGE_DOWN
+ GLFW_KEY_HOME -> Keys.HOME
+ GLFW_KEY_END -> Keys.END
+
+ GLFW_KEY_RIGHT -> Keys.RIGHT
+ GLFW_KEY_LEFT -> Keys.LEFT
+ GLFW_KEY_DOWN -> Keys.DOWN
+ GLFW_KEY_UP -> Keys.UP
+
+ else -> Keys.UNKNOWN
+ }
+
+ if (action == GLFW_PRESS) {
+ polyUI.inputManager.keyDown(key)
+ } else {
+ polyUI.inputManager.keyUp(key)
+ }
+ } else {
+ val key = when (keyCode) {
+ GLFW_KEY_LEFT_SHIFT -> KeyModifiers.LSHIFT
+ GLFW_KEY_LEFT_CONTROL -> KeyModifiers.LCONTROL
+ GLFW_KEY_LEFT_ALT -> KeyModifiers.LALT
+ GLFW_KEY_LEFT_SUPER -> KeyModifiers.LMETA
+ GLFW_KEY_RIGHT_SHIFT -> KeyModifiers.RSHIFT
+ GLFW_KEY_RIGHT_CONTROL -> KeyModifiers.RCONTROL
+ GLFW_KEY_RIGHT_ALT -> KeyModifiers.RALT
+ GLFW_KEY_RIGHT_SUPER -> KeyModifiers.RMETA
+ else -> KeyModifiers.UNKNOWN
+ }
+
+ if (action == GLFW_PRESS) {
+ polyUI.inputManager.addModifier(key.value)
+ } else {
+ polyUI.inputManager.removeModifier(key.value)
+ }
+ }
+ return@glfwSetKeyCallback
+ }
+ if (action == GLFW_PRESS) {
+ polyUI.inputManager.keyDown(keyCode)
+ } else {
+ polyUI.inputManager.keyUp(keyCode)
+ }
+ }
+
+ glfwSetMouseButtonCallback(handle) { _, button, action, _ ->
+ if (action == GLFW_PRESS) {
+ polyUI.inputManager.mousePressed(button)
+ } else if (action == GLFW_RELEASE) polyUI.inputManager.mouseReleased(button)
+ }
+
+ glfwSetCharCallback(handle) { _, codepoint ->
+ polyUI.inputManager.keyTyped(codepoint.toChar())
+ }
+
+ var ran = false
+ glfwSetScrollCallback(handle) { _, x, y ->
+ // asm: small scroll amounts are usually trackpads
+ if (!ran && (y < 1.0 && x < 1.0) && PolyUI.isOnMac && !polyUI.settings.naturalScrolling) {
+ LOGGER.info("Enabled natural scrolling as it has been guessed to be a trackpad on macOS.")
+ polyUI.settings.naturalScrolling = true
+ }
+ ran = true
+ polyUI.inputManager.mouseScrolled(x.toFloat(), y.toFloat())
+ }
+
+ glfwSetDropCallback(handle) { _, count, names ->
+ val files = Array(count) {
+ Paths.get(GLFWDropCallback.getName(names, it))
+ }
+ polyUI.inputManager.filesDropped(files)
+ }
+
+ glfwSetWindowFocusCallback(handle) { _, focused ->
+ if (polyUI.settings.unfocusedFPS != 0) {
+ fpsCap = if (focused) polyUI.settings.maxFPS.toDouble() else polyUI.settings.unfocusedFPS.toDouble()
+ }
+ if (focused) {
+ polyUI.master.needsRedraw = true
+ }
+ }
+ }
+
+ override fun open(polyUI: PolyUI): Window {
+ polyUI.window = this
+ glfwSetTime(0.0)
+ glfwSwapInterval(if (polyUI.settings.enableVSync) 1 else 0)
+
+ createCallbacks(polyUI)
+
+ MemoryStack.stackPush().use {
+ val wbuf = it.mallocInt(1)
+ val hbuf = it.mallocInt(1)
+ val sxbuf = it.mallocFloat(1)
+ val sybuf = it.mallocFloat(1)
+ glfwGetWindowContentScale(handle, sxbuf, sybuf)
+ glfwGetFramebufferSize(handle, wbuf, hbuf)
+ val sx = sxbuf[0]
+ val sy = sybuf[0]
+ val w = wbuf[0]
+ val h = hbuf[0]
+
+ pixelRatio = max(sx, sy)
+ polyUI.resize(
+ w.toFloat() / sx,
+ h.toFloat() / sy
+ )
+
+ this.width = w
+ this.height = h
+ }
+
+ val (minW, minH) = polyUI.settings.minimumSize
+ val (maxW, maxH) = polyUI.settings.maximumSize
+ glfwSetWindowSizeLimits(handle, minW.toInt(), minH.toInt(), maxW.toInt(), maxH.toInt())
+
+ if (polyUI.settings.aspectRatio.x == 0f || polyUI.settings.aspectRatio.y == 0f) {
+ val ratio = Vec2(width.toFloat(), height.toFloat()).simplifyRatio()
+ LOGGER.info("Inferred aspect ratio: ${ratio.x}:${ratio.y}")
+ polyUI.settings.aspectRatio = ratio
+ }
+ glfwSetWindowAspectRatio(handle, polyUI.settings.aspectRatio.x.toInt(), polyUI.settings.aspectRatio.y.toInt())
+
+ var time = glfwGetTime()
+ fpsCap = polyUI.settings.maxFPS.toDouble()
+ while (!glfwWindowShouldClose(handle)) {
+ val size = polyUI.size
+ val height = (size.y * pixelRatio).toInt()
+ glViewport(0, offset + (this.height - height), (size.x * pixelRatio).toInt(), height)
+ glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
+ glClearColor(0f, 0f, 0f, 0f)
+
+ val timeout = polyUI.render()
+ if (fpsCap != 0.0) {
+ val delta = glfwGetTime() - time
+ if (delta < fpsCap) {
+ Thread.sleep(((fpsCap - delta) * 1_000.0).toLong())
+ }
+ time = glfwGetTime()
+ }
+ if (polyUI.drew) glfwSwapBuffers(handle)
+
+ if (timeout == 0L) glfwPollEvents()
+ else glfwWaitEventsTimeout((timeout / 1_000_000_000.0))
+ }
+
+ polyUI.cleanup()
+ setCapabilities(null)
+ Callbacks.glfwFreeCallbacks(handle)
+ glfwTerminate()
+ glfwSetErrorCallback(null)?.free()
+ return this
+ }
+
+ override fun close() = glfwSetWindowShouldClose(handle, true)
+
+ /** set the icon of this window according to the given [icon] path. This should be a resource path that can be used by [getResourceStream].
+ *
+ * **Does not work on macOS.** This is a limitation of GLFW, and is not a bug in PolyUI.
+ *
+ * The icon should be a PNG, BMP or JPG, and in a 2x size (i.e. 16x16, 32x32, 128x128, etc)
+ * @throws Exception if the image does not exist, or a different IO error occurs.
+ *
+ * [SIGSEGV](https://en.wikipedia.org/wiki/Segmentation_fault) - if something else goes wrong in this method. Your JVM will crash blaming `[libglfw.so+0x211xx]` or something.
+ */
+ fun setIcon(icon: String) {
+ val w = IntArray(1)
+ val h = IntArray(1)
+ val data = STBImage.stbi_load_from_memory(getResourceStream(icon).toByteArray().toDirectByteBuffer(), w, h, IntArray(1), 4)
+ ?: throw Exception("error occurred while loading icon!")
+ glfwSetWindowIcon(handle, GLFWImage.malloc(1).put(0, GLFWImage.malloc().set(w[0], h[0], data)))
+ }
+
+ override fun supportsRenderPausing() = true
+
+ override fun getClipboard() = glfwGetClipboardString(handle)
+
+ override fun setClipboard(text: String?) = if (text != null) glfwSetClipboardString(handle, text as CharSequence) else Unit
+
+ override fun setCursor(cursor: Cursor) {
+ glfwSetCursor(
+ handle,
+ glfwCreateStandardCursor(
+ when (cursor) {
+ Cursor.Pointer -> GLFW_ARROW_CURSOR
+ Cursor.Clicker -> GLFW_POINTING_HAND_CURSOR
+ Cursor.Text -> GLFW_IBEAM_CURSOR
+ },
+ ),
+ )
+ }
+
+ override fun breakPause() {
+// glfwPostEmptyEvent()
+ }
+
+ override fun getKeyName(key: Int) = glfwGetKeyName(key, glfwGetKeyScancode(key)) ?: "Unknown"
+
+ fun fullscreen() {
+ glfwGetVideoMode(glfwGetPrimaryMonitor())?.let {
+ glfwSetWindowSize(handle, it.width(), it.height())
+ }
+ }
+}
diff --git a/polyui/src/test/kotlin/MarkdownTest.kt b/polyui/src/test/kotlin/MarkdownTest.kt
new file mode 100644
index 0000000..5d80d02
--- /dev/null
+++ b/polyui/src/test/kotlin/MarkdownTest.kt
@@ -0,0 +1,59 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+import dev.dediamondpro.minemark.polyui.MarkdownComponent
+import org.polyfrost.polyui.PolyUI
+import org.polyfrost.polyui.renderer.impl.GLFWWindow
+import org.polyfrost.polyui.renderer.impl.NVGRenderer
+import org.polyfrost.polyui.unit.Vec2
+import org.polyfrost.polyui.unit.by
+
+fun main() {
+ val window = GLFWWindow("PolyUI Markdown Test", 800, 1000)
+ val polyUI =
+ PolyUI(
+ MarkdownComponent(
+ markdown = "Test **string** *with* a
newline because ~~why~~ not and also a "
+ + "[link](https://polyfrost.org), this should also automatically wrap if I make this text long "
+ + "enough
"
+ + "Image: ![](https://picsum.photos/100/100.jpg)
"
+ + "HTML Image: "
+ + "\n# Heading 1"
+ + "\n## Heading 2"
+ + "\n### Heading 3"
+ + "\n#### Heading 4"
+ + "\n##### Heading 5"
+ + "\n---"
+ + "\nThis `is an inline` codeblock!"
+ + "\n```\nAnd this one isn't inline\nand has multiple lines!\n```"
+ + "\n> And this is a blockquote
because why not?"
+ + "\n1. And an ordered list"
+ + "\n - With an unordered list inside"
+ + "\n - like this"
+ + "\n2. :)"
+ + "\n\n| Hi | Test | E | E | E |\n" +
+ "|----|------|---------|---|---|\n" +
+ "| E | E | E | E | |\n" +
+ "| E | E | EEEEEEE | | E |\n" +
+ "| E | E | E | E | |",
+ size = Vec2(600f, 1000f)
+ ),
+ renderer = NVGRenderer,
+ size = 800f by 1000f
+ )
+ window.open(polyUI)
+}
\ No newline at end of file
diff --git a/polyui/src/test/kotlin/NVGRenderer.kt b/polyui/src/test/kotlin/NVGRenderer.kt
new file mode 100644
index 0000000..26a7e09
--- /dev/null
+++ b/polyui/src/test/kotlin/NVGRenderer.kt
@@ -0,0 +1,510 @@
+/*
+ * This file is part of MineMark
+ * Copyright (C) 2023-2024 DeDiamondPro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License Version 3 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package org.polyfrost.polyui.renderer.impl
+
+import org.apache.logging.log4j.LogManager
+import org.lwjgl.nanovg.NSVGImage
+import org.lwjgl.nanovg.NVGColor
+import org.lwjgl.nanovg.NVGPaint
+import org.lwjgl.nanovg.NanoSVG.*
+import org.lwjgl.nanovg.NanoVG.*
+import org.lwjgl.nanovg.NanoVGGL3.*
+import org.lwjgl.stb.STBImage.stbi_failure_reason
+import org.lwjgl.stb.STBImage.stbi_load_from_memory
+import org.lwjgl.system.MemoryUtil
+import org.polyfrost.polyui.PolyUI
+import org.polyfrost.polyui.data.Font
+import org.polyfrost.polyui.data.PolyImage
+import org.polyfrost.polyui.renderer.Renderer
+import org.polyfrost.polyui.unit.Vec2
+import org.polyfrost.polyui.utils.*
+import java.nio.ByteBuffer
+import java.util.IdentityHashMap
+import org.polyfrost.polyui.color.PolyColor as Color
+
+object NVGRenderer : Renderer {
+ @JvmStatic
+ private val LOGGER = LogManager.getLogger("PolyUI/NVGRenderer")
+ private val nvgPaint: NVGPaint = NVGPaint.malloc()
+ private val nvgColor: NVGColor = NVGColor.malloc()
+ private val nvgColor2: NVGColor = NVGColor.malloc()
+ private val images = HashMap()
+ private val svgs = HashMap>()
+ private val fonts = IdentityHashMap()
+ private var defaultFont: NVGFont? = null
+ private var defaultImageData: ByteArray? = null
+ private var defaultImage = 0
+ private var vg: Long = 0L
+ private var raster: Long = 0L
+ private var drawing = false
+ private val queue = ArrayList<() -> Unit>()
+
+ // ByteBuffer.of("px\u0000")
+ private val PIXELS: ByteBuffer = MemoryUtil.memAlloc(3).put(112).put(120).put(0).flip() as ByteBuffer
+ private val errorHandler: (Throwable) -> Unit = { LOGGER.error("failed to load resource!", it) }
+
+ override fun init() {
+ if (vg == 0L) vg = nvgCreate(NVG_ANTIALIAS)
+ if (raster == 0L) raster = nsvgCreateRasterizer()
+ require(vg != 0L) { "Could not initialize NanoVG" }
+ require(raster != 0L) { "Could not initialize NanoSVG" }
+
+ if (defaultFont == null) {
+ val font = PolyUI.defaultFonts.regular
+ val fdata = font.load().toDirectByteBuffer()
+ val fit = NVGFont(nvgCreateFontMem(vg, font.name, fdata, false), fdata)
+ this.defaultFont = fit
+ fonts[font] = fit
+ }
+
+ if (defaultImage == 0) {
+ val iImage = PolyUI.defaultImage
+ val iData = iImage.load()
+ defaultImageData = iData
+ this.defaultImage = loadImage(iImage, iData.toDirectByteBuffer())
+ require(this.defaultImage != 0) { "NanoVG failed to initialize default image" }
+ }
+ }
+
+ override fun beginFrame(width: Float, height: Float, pixelRatio: Float) {
+ if (drawing) throw IllegalStateException("Already drawing")
+ queue.fastRemoveIfReversed { it(); true }
+ nvgBeginFrame(vg, width, height, pixelRatio)
+ drawing = true
+ }
+
+ override fun endFrame() {
+ if (!drawing) throw IllegalStateException("Not drawing")
+ nvgEndFrame(vg)
+ drawing = false
+ }
+
+ override fun globalAlpha(alpha: Float) = nvgGlobalAlpha(vg, alpha)
+
+ override fun translate(x: Float, y: Float) = nvgTranslate(vg, x, y)
+
+ override fun scale(sx: Float, sy: Float, px: Float, py: Float) = nvgScale(vg, sx, sy)
+
+ override fun rotate(angleRadians: Double, px: Float, py: Float) = nvgRotate(vg, angleRadians.toFloat())
+
+ override fun skewX(angleRadians: Double, px: Float, py: Float) = nvgSkewX(vg, angleRadians.toFloat())
+
+ override fun skewY(angleRadians: Double, px: Float, py: Float) = nvgSkewY(vg, angleRadians.toFloat())
+
+ override fun transformsWithPoint() = false
+
+ override fun push() = nvgSave(vg)
+
+ override fun pop() = nvgRestore(vg)
+
+ override fun pushScissor(x: Float, y: Float, width: Float, height: Float) = nvgScissor(vg, x, y, width, height)
+
+ override fun pushScissorIntersecting(x: Float, y: Float, width: Float, height: Float) = nvgIntersectScissor(vg, x, y, width, height)
+
+ override fun popScissor() = nvgResetScissor(vg)
+
+ override fun text(
+ font: Font,
+ x: Float,
+ y: Float,
+ text: String,
+ color: Color,
+ fontSize: Float,
+ ) {
+ if (color.transparent) return
+ nvgBeginPath(vg)
+ nvgFontSize(vg, fontSize)
+ nvgFontFaceId(vg, getFont(font))
+ nvgTextAlign(vg, NVG_ALIGN_LEFT or NVG_ALIGN_TOP)
+ color(color)
+ nvgFillColor(vg, nvgColor)
+ nvgText(vg, x, y, text)
+ }
+
+ override fun image(
+ image: PolyImage,
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ colorMask: Int,
+ topLeftRadius: Float,
+ topRightRadius: Float,
+ bottomLeftRadius: Float,
+ bottomRightRadius: Float,
+ ) {
+ nvgImagePattern(vg, x, y, width, height, 0f, getImage(image, width, height), 1f, nvgPaint)
+ if (colorMask != 0) {
+ nvgARGB(colorMask, nvgPaint.innerColor())
+ }
+ nvgBeginPath(vg)
+ nvgRoundedRectVarying(
+ vg,
+ x,
+ y,
+ width,
+ height,
+ topLeftRadius,
+ topRightRadius,
+ bottomRightRadius,
+ bottomLeftRadius,
+ )
+ nvgFillPaint(vg, nvgPaint)
+ nvgFill(vg)
+ }
+
+ override fun delete(font: Font?) {
+ fonts.remove(font)
+ }
+
+ override fun delete(image: PolyImage?) {
+ images.remove(image).also {
+ if (it != null) {
+ nvgDeleteImage(vg, it)
+ return
+ }
+ }
+ svgs.remove(image).also {
+ if (it != null) {
+ nsvgDelete(it.first)
+ it.second.forEach { _, handle ->
+ nvgDeleteImage(vg, handle)
+ }
+ }
+ }
+ }
+
+ override fun initImage(image: PolyImage, size: Vec2) {
+ getImage(image, size.x, size.y)
+ }
+
+ override fun rect(
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ color: Color,
+ topLeftRadius: Float,
+ topRightRadius: Float,
+ bottomLeftRadius: Float,
+ bottomRightRadius: Float,
+ ) {
+ if (color.transparent) return
+ // note: nvg checks params and draws classic rect if 0, so we don't need to
+ nvgBeginPath(vg)
+ nvgRoundedRectVarying(
+ vg,
+ x,
+ y,
+ width,
+ height,
+ topLeftRadius,
+ topRightRadius,
+ bottomRightRadius,
+ bottomLeftRadius,
+ )
+ if (color(color, x, y, width, height)) {
+ nvgFillPaint(vg, nvgPaint)
+ } else {
+ nvgFillColor(vg, nvgColor)
+ }
+ nvgFill(vg)
+ }
+
+ override fun hollowRect(
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ color: Color,
+ lineWidth: Float,
+ topLeftRadius: Float,
+ topRightRadius: Float,
+ bottomLeftRadius: Float,
+ bottomRightRadius: Float,
+ ) {
+ if (color.transparent) return
+ nvgBeginPath(vg)
+ nvgRoundedRectVarying(
+ vg,
+ x,
+ y,
+ width,
+ height,
+ topLeftRadius,
+ topRightRadius,
+ bottomRightRadius,
+ bottomLeftRadius,
+ )
+ nvgStrokeWidth(vg, lineWidth)
+ if (color(color, x, y, width, height)) {
+ nvgStrokePaint(vg, nvgPaint)
+ } else {
+ nvgStrokeColor(vg, nvgColor)
+ }
+ nvgStroke(vg)
+ }
+
+ override fun line(x1: Float, y1: Float, x2: Float, y2: Float, color: Color, width: Float) {
+ if (color.transparent) return
+ nvgBeginPath(vg)
+ nvgMoveTo(vg, x1, y1)
+ nvgLineTo(vg, x2, y2)
+ nvgStrokeWidth(vg, width)
+ if (color(color, x1, y1, x2, y2)) {
+ nvgStrokePaint(vg, nvgPaint)
+ } else {
+ nvgStrokeColor(vg, nvgColor)
+ }
+ nvgStroke(vg)
+ }
+
+ override fun dropShadow(
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ blur: Float,
+ spread: Float,
+ radius: Float,
+ ) {
+ nvgBoxGradient(vg, x - spread, y - spread, width + spread * 2f, height + spread * 2f, radius + spread, blur, nvgColor, nvgColor2, nvgPaint)
+ nvgBeginPath(vg)
+ nvgRoundedRect(vg, x - spread, y - spread - blur, width + spread * 2f + blur * 2f, height + spread * 2f + blur * 2f, radius + spread)
+ nvgRoundedRect(vg, x, y, width, height, radius)
+ nvgPathWinding(vg, NVG_HOLE)
+ nvgFillPaint(vg, nvgPaint)
+ nvgFill(vg)
+ }
+
+ @Suppress("NAME_SHADOWING")
+ override fun textBounds(font: Font, text: String, fontSize: Float): Vec2 {
+ // nanovg trims single whitespace, so add an extra one (lol)
+ var text = text
+ if (text.endsWith(' ')) {
+ text += ' '
+ }
+ val out = FloatArray(4)
+ nvgFontFaceId(vg, getFont(font))
+ nvgTextAlign(vg, NVG_ALIGN_TOP or NVG_ALIGN_LEFT)
+ nvgFontSize(vg, fontSize)
+ nvgTextBounds(vg, 0f, 0f, text, out)
+ val w = out[2] - out[0]
+ val h = out[3] - out[1]
+ return Vec2(w, h)
+ }
+
+ private fun color(color: Color) {
+ nvgARGB(color.argb, nvgColor)
+ if (color is Color.Gradient) {
+ nvgARGB(color.argb2, nvgColor2)
+ }
+ }
+
+ private fun nvgARGB(argb: Int, ptr: NVGColor) {
+ nvgRGBA(
+ (argb shr 16 and 0xFF).toByte(),
+ (argb shr 8 and 0xFF).toByte(),
+ (argb and 0xFF).toByte(),
+ (argb shr 24 and 0xFF).toByte(),
+ ptr
+ )
+
+ }
+
+ private fun color(
+ color: Color,
+ x: Float,
+ y: Float,
+ width: Float,
+ height: Float,
+ ): Boolean {
+ color(color)
+ if (color !is Color.Gradient) return false
+ when (color.type) {
+ is Color.Gradient.Type.TopToBottom -> nvgLinearGradient(
+ vg,
+ x,
+ y,
+ x,
+ y + height,
+ nvgColor,
+ nvgColor2,
+ nvgPaint,
+ )
+
+ is Color.Gradient.Type.TopLeftToBottomRight -> nvgLinearGradient(
+ vg,
+ x,
+ y,
+ x + width,
+ y + height,
+ nvgColor,
+ nvgColor2,
+ nvgPaint,
+ )
+
+ is Color.Gradient.Type.LeftToRight -> nvgLinearGradient(
+ vg,
+ x,
+ y,
+ x + width,
+ y,
+ nvgColor,
+ nvgColor2,
+ nvgPaint,
+ )
+
+ is Color.Gradient.Type.BottomLeftToTopRight -> nvgLinearGradient(
+ vg,
+ x,
+ y + height,
+ x + width,
+ y,
+ nvgColor,
+ nvgColor2,
+ nvgPaint,
+ )
+
+ is Color.Gradient.Type.Radial -> {
+ val type = color.type as Color.Gradient.Type.Radial
+ nvgRadialGradient(
+ vg,
+ if (type.centerX == -1f) x + (width / 2f) else type.centerX,
+ if (type.centerY == -1f) y + (height / 2f) else type.centerY,
+ type.innerRadius,
+ type.outerRadius,
+ nvgColor,
+ nvgColor2,
+ nvgPaint,
+ )
+ }
+
+ is Color.Gradient.Type.Box -> nvgBoxGradient(
+ vg,
+ x,
+ y,
+ width,
+ height,
+ (color.type as Color.Gradient.Type.Box).radius,
+ (color.type as Color.Gradient.Type.Box).feather,
+ nvgColor,
+ nvgColor2,
+ nvgPaint,
+ )
+ }
+ return true
+ }
+
+ private fun getFont(font: Font): Int {
+ if (font.loadSync) return getFontSync(font)
+ return fonts.getOrPut(font) {
+ font.loadAsync(errorHandler = errorHandler) { data ->
+ val it = data.toDirectByteBuffer()
+ queue.add { fonts[font] = NVGFont(nvgCreateFontMem(vg, font.name, it, false), it) }
+ }
+ defaultFont!!
+ }.id
+ }
+
+ private fun getFontSync(font: Font): Int {
+ return fonts.getOrPut(font) {
+ val data = font.load { errorHandler(it); return@getOrPut defaultFont!! }.toDirectByteBuffer()
+ NVGFont(nvgCreateFontMem(vg, font.name, data, false), data)
+ }.id
+ }
+
+ private fun getImage(image: PolyImage, width: Float, height: Float): Int {
+ if (image.loadSync || (width == 0f && height == 0f)) return getImageSync(image, width, height)
+ return when (image.type) {
+ PolyImage.Type.Vector -> {
+ val (svg, map) = svgs[image] ?: run {
+ image.loadAsync(errorHandler) {
+ queue.add { svgLoad(image, it.toDirectByteBufferNT()) }
+ }
+ return defaultImage
+ }
+ map.getOrPut(width.hashCode() * 31 + height.hashCode()) { svgResize(svg, width, height) }
+ }
+
+ PolyImage.Type.Raster -> {
+ images.getOrPut(image) {
+ image.loadAsync(errorHandler) {
+ queue.add { images[image] = loadImage(image, it.toDirectByteBuffer()) }
+ }
+ defaultImage
+ }
+ }
+
+ else -> throw NoWhenBranchMatchedException("Please specify image type for $image")
+ }
+ }
+
+ private fun getImageSync(image: PolyImage, width: Float, height: Float): Int {
+ return when (image.type) {
+ PolyImage.Type.Vector -> {
+ val (svg, map) = svgs[image] ?: return svgLoad(image, image.load { errorHandler(it); defaultImageData!! }.toDirectByteBufferNT())
+ if (!image.size.isPositive) PolyImage.setImageSize(image, Vec2(svg.width(), svg.height()))
+ map.getOrPut(width.hashCode() * 31 + height.hashCode()) { svgResize(svg, width, height) }
+ }
+
+ PolyImage.Type.Raster -> {
+ images.getOrPut(image) { loadImage(image, image.load { errorHandler(it); defaultImageData!! }.toDirectByteBuffer()) }
+ }
+
+ else -> throw NoWhenBranchMatchedException("Please specify image type for $image")
+ }
+ }
+
+ private fun svgLoad(image: PolyImage, data: ByteBuffer): Int {
+ val svg = nsvgParse(data, PIXELS, 96f) ?: throw IllegalStateException("Failed to parse SVG: ${image.resourcePath}")
+ val map = Int2IntMap(4)
+ if (!image.size.isPositive) PolyImage.setImageSize(image, Vec2(svg.width(), svg.height()))
+ val o = svgResize(svg, svg.width(), svg.height())
+ map[image.size.hashCode()] = o
+ svgs[image] = svg to map
+ return o
+ }
+
+ private fun svgResize(svg: NSVGImage, width: Float, height: Float): Int {
+ val wi = ((if (width == 0f) svg.width() else width) * 2f).toInt()
+ val hi = ((if (height == 0f) svg.height() else height) * 2f).toInt()
+ val dst = MemoryUtil.memAlloc(wi * hi * 4)
+ val scale = cl1(width / svg.width(), height / svg.height()) * 2f
+ nsvgRasterize(raster, svg, 0f, 0f, scale, dst, wi, hi, wi * 4)
+ return nvgCreateImageRGBA(vg, wi, hi, 0, dst)
+ }
+
+ private fun loadImage(image: PolyImage, data: ByteBuffer): Int {
+ val w = IntArray(1)
+ val h = IntArray(1)
+ val d = stbi_load_from_memory(data, w, h, IntArray(1), 4) ?: throw IllegalStateException("Failed to load image ${image.resourcePath}: ${stbi_failure_reason()}")
+ if (!image.size.isPositive) PolyImage.setImageSize(image, Vec2(w[0].toFloat(), h[0].toFloat()))
+ return nvgCreateImageRGBA(vg, w[0], h[0], 0, d)
+ }
+
+ override fun cleanup() {
+ nvgColor.free()
+ nvgColor2.free()
+ nvgPaint.free()
+ nsvgDeleteRasterizer(raster)
+ nvgDelete(vg)
+ }
+
+ private data class NVGFont(val id: Int, val data: ByteBuffer)
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8c102f7..816504d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -22,6 +22,7 @@ dependencyResolutionManagement {
}
include(":elementa")
+include(":polyui")
includeBuild("minecraft")
includeBuild(".")
diff --git a/src/main/java/dev/dediamondpro/minemark/elements/MineMarkElement.java b/src/main/java/dev/dediamondpro/minemark/elements/MineMarkElement.java
index 0346780..b7b8df9 100644
--- a/src/main/java/dev/dediamondpro/minemark/elements/MineMarkElement.java
+++ b/src/main/java/dev/dediamondpro/minemark/elements/MineMarkElement.java
@@ -93,6 +93,16 @@ public void onMouseClicked(float x, float y, MouseButton button, float mouseX, f
this.onMouseClickedInternal(button, mouseX - x, mouseY - y);
}
+ /**
+ * Function to see if this element needs to regenerate its layout
+ *
+ * @param width The width the element should draw at
+ * @return If the current layout is out of date and needs to be regenerated
+ */
+ public boolean needsLayoutRegeneration(float width) {
+ return width != lastWidth;
+ }
+
/**
* Method to call if you want the layout to regenerate the next time
* {@link MineMarkElement#beforeDraw(float, float, float, float, float, Object)} is called.