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.