diff --git a/components/extensions/build.gradle.kts b/components/extensions/build.gradle.kts index 010aecd3..249409b3 100644 --- a/components/extensions/build.gradle.kts +++ b/components/extensions/build.gradle.kts @@ -1,3 +1,8 @@ plugins { alias(libs.plugins.kotlin.jvm) } + +dependencies { + testImplementation(libs.bundles.test) + testRuntimeOnly(libs.junit.launcher) +} diff --git a/components/extensions/src/main/kotlin/io/github/composegears/valkyrie/extensions/Any.kt b/components/extensions/src/main/kotlin/io/github/composegears/valkyrie/extensions/Any.kt index 9017bcbc..1d051933 100644 --- a/components/extensions/src/main/kotlin/io/github/composegears/valkyrie/extensions/Any.kt +++ b/components/extensions/src/main/kotlin/io/github/composegears/valkyrie/extensions/Any.kt @@ -1,3 +1,5 @@ package io.github.composegears.valkyrie.extensions -inline fun Any?.castOrNull(): T? = this as? T +inline fun Any?.safeAs(): T? = this as? T + +inline fun Any?.cast(): T = this as T diff --git a/components/extensions/src/main/kotlin/io/github/composegears/valkyrie/extensions/Color.kt b/components/extensions/src/main/kotlin/io/github/composegears/valkyrie/extensions/Color.kt new file mode 100644 index 00000000..b8f1179c --- /dev/null +++ b/components/extensions/src/main/kotlin/io/github/composegears/valkyrie/extensions/Color.kt @@ -0,0 +1,7 @@ +package io.github.composegears.valkyrie.extensions + +fun String.toColorInt() = this + .trimStart('#') + .padStart(8, 'F') + .toLong(16) + .toInt() diff --git a/components/extensions/src/test/kotlin/io/github/composegears/valkyrie/extensions/ColorTest.kt b/components/extensions/src/test/kotlin/io/github/composegears/valkyrie/extensions/ColorTest.kt new file mode 100644 index 00000000..ebdc8a97 --- /dev/null +++ b/components/extensions/src/test/kotlin/io/github/composegears/valkyrie/extensions/ColorTest.kt @@ -0,0 +1,15 @@ +package io.github.composegears.valkyrie.extensions + +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.jupiter.api.Test + +class ColorTest { + + @Test + fun `string to color int`() { + assertThat("#FF000000".toColorInt()).isEqualTo(-16777216) + assertThat("FF000000".toColorInt()).isEqualTo(-16777216) + assertThat("000000".toColorInt()).isEqualTo(-16777216) + } +} diff --git a/components/ir/build.gradle.kts b/components/ir/build.gradle.kts new file mode 100644 index 00000000..010aecd3 --- /dev/null +++ b/components/ir/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} diff --git a/components/ir/src/main/kotlin/io/github/composegears/valkyrie/ir/IrImageVector.kt b/components/ir/src/main/kotlin/io/github/composegears/valkyrie/ir/IrImageVector.kt new file mode 100644 index 00000000..496f37f0 --- /dev/null +++ b/components/ir/src/main/kotlin/io/github/composegears/valkyrie/ir/IrImageVector.kt @@ -0,0 +1,178 @@ +package io.github.composegears.valkyrie.ir + +data class IrImageVector( + val name: String = "", + val autoMirror: Boolean = false, + val defaultWidth: Float, + val defaultHeight: Float, + val viewportWidth: Float, + val viewportHeight: Float, + val vectorNodes: List, +) + +sealed interface IrVectorNode { + data class IrGroup(val nodes: List) : IrVectorNode + + data class IrPath( + val name: String = "", + val fill: IrFill? = null, + val fillAlpha: Float = 1f, + val stroke: IrStroke? = null, + val strokeAlpha: Float = 1f, + val strokeLineWidth: Float = 0f, + val strokeLineCap: IrStrokeLineCap = IrStrokeLineCap.Butt, + val strokeLineJoin: IrStrokeLineJoin = IrStrokeLineJoin.Miter, + val strokeLineMiter: Float = 4f, + val pathFillType: IrPathFillType = IrPathFillType.NonZero, + val nodes: List, + ) : IrVectorNode +} + +enum class IrPathFillType { + EvenOdd, + NonZero, +} + +enum class IrStrokeLineCap { + Butt, + Round, + Square, +} + +enum class IrStrokeLineJoin { + Miter, + Round, + Bevel, +} + +sealed interface IrFill { + data class Color(val colorHex: String) : IrFill + + data class LinearGradient( + val startY: Float, + val startX: Float, + val endY: Float, + val endX: Float, + val colorStops: List = listOf(), + ) : IrFill + + data class RadialGradient( + val radius: Float, + val centerX: Float, + val centerY: Float, + val colorStops: List = listOf(), + ) : IrFill + + data class ColorStop( + val offset: Float, + val color: String, + ) +} + +sealed interface IrStroke { + data class Color(val colorHex: String) : IrStroke +} + +sealed interface IrPathNode { + + data object Close : IrPathNode + data class RelativeMoveTo( + val x: Float, + val y: Float, + ) : IrPathNode + + data class MoveTo( + val x: Float, + val y: Float, + ) : IrPathNode + + data class RelativeLineTo( + val x: Float, + val y: Float, + ) : IrPathNode + + data class LineTo( + val x: Float, + val y: Float, + ) : IrPathNode + + data class RelativeHorizontalTo(val x: Float) : IrPathNode + data class HorizontalTo(val x: Float) : IrPathNode + data class RelativeVerticalTo(val y: Float) : IrPathNode + data class VerticalTo(val y: Float) : IrPathNode + data class RelativeCurveTo( + val dx1: Float, + val dy1: Float, + val dx2: Float, + val dy2: Float, + val dx3: Float, + val dy3: Float, + ) : IrPathNode + + data class CurveTo( + val x1: Float, + val y1: Float, + val x2: Float, + val y2: Float, + val x3: Float, + val y3: Float, + ) : IrPathNode + + data class RelativeReflectiveCurveTo( + val x1: Float, + val y1: Float, + val x2: Float, + val y2: Float, + ) : IrPathNode + + data class ReflectiveCurveTo( + val x1: Float, + val y1: Float, + val x2: Float, + val y2: Float, + ) : IrPathNode + + data class RelativeQuadTo( + val x1: Float, + val y1: Float, + val x2: Float, + val y2: Float, + ) : IrPathNode + + data class QuadTo( + val x1: Float, + val y1: Float, + val x2: Float, + val y2: Float, + ) : IrPathNode + + data class RelativeReflectiveQuadTo( + val x: Float, + val y: Float, + ) : IrPathNode + + data class ReflectiveQuadTo( + val x: Float, + val y: Float, + ) : IrPathNode + + data class RelativeArcTo( + val horizontalEllipseRadius: Float, + val verticalEllipseRadius: Float, + val theta: Float, + val isMoreThanHalf: Boolean, + val isPositiveArc: Boolean, + val arcStartDx: Float, + val arcStartDy: Float, + ) : IrPathNode + + data class ArcTo( + val horizontalEllipseRadius: Float, + val verticalEllipseRadius: Float, + val theta: Float, + val isMoreThanHalf: Boolean, + val isPositiveArc: Boolean, + val arcStartX: Float, + val arcStartY: Float, + ) : IrPathNode +} diff --git a/components/psi/iconpack/src/main/kotlin/io/github/composegears/valkyrie/psi/iconpack/IconPackPsiParser.kt b/components/psi/iconpack/src/main/kotlin/io/github/composegears/valkyrie/psi/iconpack/IconPackPsiParser.kt index b52d9177..a70a6344 100644 --- a/components/psi/iconpack/src/main/kotlin/io/github/composegears/valkyrie/psi/iconpack/IconPackPsiParser.kt +++ b/components/psi/iconpack/src/main/kotlin/io/github/composegears/valkyrie/psi/iconpack/IconPackPsiParser.kt @@ -3,7 +3,7 @@ package io.github.composegears.valkyrie.psi.iconpack import com.intellij.openapi.project.Project import com.intellij.psi.PsiManager import com.intellij.testFramework.LightVirtualFile -import io.github.composegears.valkyrie.extensions.castOrNull +import io.github.composegears.valkyrie.extensions.safeAs import java.nio.file.Path import kotlin.io.path.name import kotlin.io.path.readText @@ -23,7 +23,7 @@ object IconPackPsiParser { fun extractIconPack(path: Path, project: Project): IconPackInfo? { val ktFile = PsiManager.getInstance(project) .findFile(LightVirtualFile(path.name, KotlinFileType.INSTANCE, path.readText())) - .castOrNull() ?: return null + .safeAs() ?: return null var iconPackName: String? = null val nestedPacks = mutableListOf() diff --git a/components/psi/imagevector/build.gradle.kts b/components/psi/imagevector/build.gradle.kts new file mode 100644 index 00000000..9ac43c1e --- /dev/null +++ b/components/psi/imagevector/build.gradle.kts @@ -0,0 +1,26 @@ +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.jetbrains.intellij.module) + alias(libs.plugins.jetbrains.compose) + alias(libs.plugins.kotlin.compose) +} + +dependencies { + implementation(projects.components.extensions) + implementation(projects.components.ir) + + implementation(compose.ui) + + testImplementation(compose.material3) + testImplementation(libs.bundles.test) + testRuntimeOnly(libs.junit.launcher) + // https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-faq.html#junit5-test-framework-refers-to-junit4 + testRuntimeOnly(libs.junit4) + + intellijPlatform { + testFramework(TestFrameworkType.Platform) + testFramework(TestFrameworkType.JUnit5) + } +} diff --git a/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/extension/PsiElement.kt b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/extension/PsiElement.kt new file mode 100644 index 00000000..a6d4460a --- /dev/null +++ b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/extension/PsiElement.kt @@ -0,0 +1,10 @@ +package io.github.composegears.valkyrie.psi.extension + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil + +inline fun PsiElement.childrenOfType(): Collection = + PsiTreeUtil.findChildrenOfType(this, T::class.java) + +inline fun PsiElement.childOfType(): T? = + PsiTreeUtil.findChildOfType(this, T::class.java) diff --git a/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/ImageVectorPsiParser.kt b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/ImageVectorPsiParser.kt new file mode 100644 index 00000000..6f203ba4 --- /dev/null +++ b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/ImageVectorPsiParser.kt @@ -0,0 +1,28 @@ +package io.github.composegears.valkyrie.psi.imagevector + +import androidx.compose.ui.graphics.vector.ImageVector +import io.github.composegears.valkyrie.psi.imagevector.parser.MaterialImageVectorPsiParser +import io.github.composegears.valkyrie.psi.imagevector.parser.RegularImageVectorPsiParser +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtImportList + +object ImageVectorPsiParser { + + fun parseToImageVector(ktFile: KtFile): ImageVector? { + val isMaterial = ktFile.importList?.isMaterial() ?: return null + + return when { + isMaterial -> MaterialImageVectorPsiParser.parse(ktFile) + else -> RegularImageVectorPsiParser.parse(ktFile) + } + } + + private fun KtImportList.isMaterial(): Boolean { + return imports.any { + val fqName = it.importedFqName + + fqName?.asString() == "androidx.compose.material.icons.materialPath" || + fqName?.asString() == "androidx.compose.material.icons.materialFilled" + } + } +} diff --git a/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/FillParser.kt b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/FillParser.kt new file mode 100644 index 00000000..f1f4e6b0 --- /dev/null +++ b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/FillParser.kt @@ -0,0 +1,137 @@ +package io.github.composegears.valkyrie.psi.imagevector.common + +import io.github.composegears.valkyrie.extensions.safeAs +import io.github.composegears.valkyrie.ir.IrFill +import io.github.composegears.valkyrie.psi.extension.childOfType +import org.jetbrains.kotlin.psi.KtBinaryExpression +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression + +internal fun KtCallExpression.parseFill(): IrFill? { + val arg = valueArguments.find { it.getArgumentName()?.asName?.identifier == "fill" } + val fillExpression = arg?.getArgumentExpression() ?: return null + + return when (fillExpression) { + is KtDotQualifiedExpression -> fillExpression.parseDotQualifiedExpression() + is KtCallExpression -> fillExpression.parseCallExpression() + else -> null + } +} + +private fun KtDotQualifiedExpression.parseDotQualifiedExpression(): IrFill? { + val ktCallExpression = childOfType() ?: return null + + return when (ktCallExpression.calleeExpression?.text) { + "linearGradient" -> ktCallExpression.parseLinearGradient() + "radialGradient" -> ktCallExpression.parseRadialGradient() + else -> null + } +} + +private fun KtCallExpression.parseCallExpression(): IrFill? { + val functionName = calleeExpression?.text + + return when (functionName) { + "SolidColor" -> parseColor() + "linearGradient" -> parseLinearGradient() + "radialGradient" -> parseRadialGradient() + else -> null + } +} + +private fun KtCallExpression.parseLinearGradient(): IrFill.LinearGradient? { + var colorStops = listOf() + var startX = 0f + var startY = 0f + var endX = 0f + var endY = 0f + + valueArguments.forEach { arg -> + val argName = arg.getArgumentName()?.asName?.identifier + val argValue = arg.getArgumentExpression() + + when (argName) { + "colorStops" -> { + colorStops = argValue.safeAs()?.parseColorStops().orEmpty() + } + "start" -> { + val startExpr = argValue.safeAs() + startX = startExpr?.valueArguments?.get(0)?.getArgumentExpression()?.text?.toFloatOrNull() ?: 0f + startY = startExpr?.valueArguments?.get(1)?.getArgumentExpression()?.text?.toFloatOrNull() ?: 0f + } + "end" -> { + val endExpr = argValue.safeAs() + endX = endExpr?.valueArguments?.get(0)?.getArgumentExpression()?.text?.toFloatOrNull() ?: 0f + endY = endExpr?.valueArguments?.get(1)?.getArgumentExpression()?.text?.toFloatOrNull() ?: 0f + } + } + } + + return when { + colorStops.isEmpty() -> null + else -> IrFill.LinearGradient( + startY = startX, + startX = startY, + endY = endX, + endX = endY, + colorStops = colorStops, + ) + } +} + +private fun KtCallExpression.parseRadialGradient(): IrFill.RadialGradient? { + var colorStops = listOf() + var centerX = 0f + var centerY = 0f + var gradientRadius = 0f + + valueArguments.forEach { arg -> + val argName = arg.getArgumentName()?.asName?.identifier + val argValue = arg.getArgumentExpression() + + when (argName) { + "colorStops" -> { + colorStops = argValue.safeAs()?.parseColorStops().orEmpty() + } + "center" -> { + val centerExpr = argValue.safeAs() + centerX = centerExpr?.valueArguments?.get(0)?.getArgumentExpression()?.text?.toFloatOrNull() ?: 0f + centerY = centerExpr?.valueArguments?.get(1)?.getArgumentExpression()?.text?.toFloatOrNull() ?: 0f + } + "radius" -> { + gradientRadius = argValue?.text?.toFloatOrNull() ?: 0f + } + } + } + + return when { + colorStops.isEmpty() -> return null + else -> IrFill.RadialGradient( + radius = gradientRadius, + centerX = centerX, + centerY = centerY, + colorStops = colorStops, + ) + } +} + +private fun KtCallExpression.parseColorStops(): List { + val colorStops = mutableListOf() + + valueArguments.forEach { arg -> + val binaryExpression = arg.getArgumentExpression().safeAs() ?: return@forEach + + val offset = binaryExpression.left?.text?.toFloatOrNull() ?: return@forEach + val colorCallExpression = binaryExpression.right.safeAs() ?: return@forEach + val color = colorCallExpression.parseColor() ?: return@forEach + + colorStops.add( + IrFill.ColorStop( + offset = offset, + color = color.colorHex, + ), + ) + } + + return colorStops +} diff --git a/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/IrToImageVector.kt b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/IrToImageVector.kt new file mode 100644 index 00000000..f0dfb390 --- /dev/null +++ b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/IrToImageVector.kt @@ -0,0 +1,198 @@ +package io.github.composegears.valkyrie.psi.imagevector.common + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import io.github.composegears.valkyrie.extensions.toColorInt +import io.github.composegears.valkyrie.ir.IrFill +import io.github.composegears.valkyrie.ir.IrImageVector +import io.github.composegears.valkyrie.ir.IrPathFillType +import io.github.composegears.valkyrie.ir.IrPathNode +import io.github.composegears.valkyrie.ir.IrStroke +import io.github.composegears.valkyrie.ir.IrStrokeLineCap +import io.github.composegears.valkyrie.ir.IrStrokeLineJoin +import io.github.composegears.valkyrie.ir.IrVectorNode.IrGroup +import io.github.composegears.valkyrie.ir.IrVectorNode.IrPath + +internal fun IrImageVector.toComposeImageVector(): ImageVector { + return ImageVector.Builder( + name = name, + defaultWidth = defaultWidth.dp, + defaultHeight = defaultHeight.dp, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + autoMirror = autoMirror, + ).apply { + vectorNodes.forEach { + when (it) { + is IrGroup -> addGroup(it) + is IrPath -> addPath(it) + } + } + }.build() +} + +private fun ImageVector.Builder.addGroup(group: IrGroup) { + group { + group.nodes.forEach { + addPath(it) + } + } +} + +private fun ImageVector.Builder.addPath(path: IrPath) { + path( + fill = path.fill.toFill(), + fillAlpha = path.fillAlpha, + stroke = path.stroke.toBrush(), + strokeAlpha = path.strokeAlpha, + strokeLineWidth = path.strokeLineWidth, + strokeLineCap = path.strokeLineCap.toLineCap(), + strokeLineJoin = path.strokeLineJoin.toStrokeJoin(), + strokeLineMiter = path.strokeLineMiter, + pathFillType = path.pathFillType.toFillType(), + ) { + path.nodes.forEach { node -> + when (node) { + is IrPathNode.ArcTo -> arcTo( + horizontalEllipseRadius = node.horizontalEllipseRadius, + verticalEllipseRadius = node.verticalEllipseRadius, + theta = node.theta, + isMoreThanHalf = node.isMoreThanHalf, + isPositiveArc = node.isPositiveArc, + x1 = node.arcStartX, + y1 = node.arcStartY, + ) + is IrPathNode.Close -> close() + is IrPathNode.CurveTo -> curveTo( + x1 = node.x1, + y1 = node.y1, + x2 = node.x2, + y2 = node.y2, + x3 = node.x3, + y3 = node.y3, + ) + is IrPathNode.HorizontalTo -> horizontalLineTo(x = node.x) + is IrPathNode.LineTo -> lineTo(x = node.x, y = node.y) + is IrPathNode.MoveTo -> moveTo(x = node.x, y = node.y) + is IrPathNode.QuadTo -> quadTo( + x1 = node.x1, + y1 = node.y1, + x2 = node.x2, + y2 = node.y2, + ) + is IrPathNode.ReflectiveCurveTo -> reflectiveCurveTo( + x1 = node.x1, + y1 = node.y1, + x2 = node.x2, + y2 = node.y2, + ) + is IrPathNode.ReflectiveQuadTo -> reflectiveQuadTo( + x1 = node.x, + y1 = node.y, + ) + is IrPathNode.RelativeArcTo -> arcToRelative( + a = node.horizontalEllipseRadius, + b = node.verticalEllipseRadius, + theta = node.theta, + isMoreThanHalf = node.isMoreThanHalf, + isPositiveArc = node.isPositiveArc, + dx1 = node.arcStartDx, + dy1 = node.arcStartDy, + ) + is IrPathNode.RelativeCurveTo -> curveToRelative( + dx1 = node.dx1, + dy1 = node.dy1, + dx2 = node.dx2, + dy2 = node.dy2, + dx3 = node.dx3, + dy3 = node.dy3, + ) + is IrPathNode.RelativeHorizontalTo -> horizontalLineToRelative(dx = node.x) + is IrPathNode.RelativeLineTo -> lineToRelative(dx = node.x, dy = node.y) + is IrPathNode.RelativeMoveTo -> moveToRelative(dx = node.x, dy = node.y) + is IrPathNode.RelativeQuadTo -> quadToRelative( + dx1 = node.x1, + dy1 = node.y1, + dx2 = node.x2, + dy2 = node.y2, + ) + is IrPathNode.RelativeReflectiveCurveTo -> reflectiveCurveToRelative( + dx1 = node.x1, + dy1 = node.y1, + dx2 = node.x2, + dy2 = node.y2, + ) + is IrPathNode.RelativeReflectiveQuadTo -> reflectiveQuadToRelative( + dx1 = node.x, + dy1 = node.y, + ) + is IrPathNode.RelativeVerticalTo -> verticalLineToRelative(dy = node.y) + is IrPathNode.VerticalTo -> verticalLineTo(y = node.y) + } + } + } +} + +private fun IrStroke?.toBrush(): Brush? { + return when (this) { + is IrStroke.Color -> SolidColor(Color(color = colorHex.toColorInt())) + else -> null + } +} + +private fun IrFill?.toFill(): Brush? { + return when (this) { + is IrFill.Color -> return SolidColor(Color(color = colorHex.toColorInt())) + is IrFill.LinearGradient -> { + Brush.linearGradient( + colorStops = colorStops.map { colorStop -> + colorStop.offset to Color(colorStop.color.toColorInt()) + }.toTypedArray(), + start = Offset(startX, startY), + end = Offset(endX, endY), + ) + } + is IrFill.RadialGradient -> { + Brush.radialGradient( + colorStops = colorStops.map { colorStop -> + colorStop.offset to Color(colorStop.color.toColorInt()) + }.toTypedArray(), + center = Offset(centerX, centerY), + radius = radius, + ) + } + else -> null + } +} + +private fun IrStrokeLineCap.toLineCap(): StrokeCap { + return when (this) { + IrStrokeLineCap.Butt -> StrokeCap.Butt + IrStrokeLineCap.Round -> StrokeCap.Round + IrStrokeLineCap.Square -> StrokeCap.Square + } +} + +private fun IrStrokeLineJoin.toStrokeJoin(): StrokeJoin { + return when (this) { + IrStrokeLineJoin.Bevel -> StrokeJoin.Bevel + IrStrokeLineJoin.Miter -> StrokeJoin.Miter + IrStrokeLineJoin.Round -> StrokeJoin.Round + } +} + +private fun IrPathFillType.toFillType(): PathFillType { + return when (this) { + IrPathFillType.EvenOdd -> PathFillType.EvenOdd + IrPathFillType.NonZero -> PathFillType.NonZero + } +} diff --git a/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/KtCallExpression.kt b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/KtCallExpression.kt new file mode 100644 index 00000000..89ffa099 --- /dev/null +++ b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/KtCallExpression.kt @@ -0,0 +1,65 @@ +package io.github.composegears.valkyrie.psi.imagevector.common + +import io.github.composegears.valkyrie.extensions.safeAs +import io.github.composegears.valkyrie.ir.IrFill +import io.github.composegears.valkyrie.ir.IrPathFillType +import io.github.composegears.valkyrie.ir.IrStroke +import io.github.composegears.valkyrie.ir.IrStrokeLineCap +import io.github.composegears.valkyrie.ir.IrStrokeLineJoin +import org.jetbrains.kotlin.psi.KtCallExpression + +internal fun KtCallExpression.extractPathFillType(): IrPathFillType { + val argument = valueArguments.find { it.getArgumentName()?.asName?.identifier == "pathFillType" } + + return when (argument?.getArgumentExpression()?.text) { + "PathFillType.NonZero", "NonZero" -> IrPathFillType.NonZero + "PathFillType.EvenOdd", "EvenOdd" -> IrPathFillType.EvenOdd + else -> IrPathFillType.NonZero + } +} + +internal fun KtCallExpression.extractStrokeJoin(): IrStrokeLineJoin { + val arg = valueArguments.find { it.getArgumentName()?.asName?.identifier == "strokeLineJoin" } + + return when (arg?.getArgumentExpression()?.text) { + "StrokeJoin.Miter", "Miter" -> IrStrokeLineJoin.Miter + "StrokeJoin.Round", "Round" -> IrStrokeLineJoin.Round + "StrokeJoin.Bevel", "Bevel" -> IrStrokeLineJoin.Bevel + else -> IrStrokeLineJoin.Miter + } +} + +internal fun KtCallExpression.extractStrokeCap(): IrStrokeLineCap { + val arg = valueArguments.find { it.getArgumentName()?.asName?.identifier == "strokeLineCap" } + + return when (arg?.getArgumentExpression()?.text) { + "StrokeCap.Butt", "Butt" -> IrStrokeLineCap.Butt + "StrokeCap.Round", "Round" -> IrStrokeLineCap.Round + "StrokeCap.Square", "Square" -> IrStrokeLineCap.Square + else -> IrStrokeLineCap.Butt + } +} + +internal fun KtCallExpression.parseFloatArg(name: String): Float? { + val arg = valueArguments.find { it.getArgumentName()?.asName?.identifier == name } + + return arg?.getArgumentExpression()?.text?.toFloatOrNull() +} + +internal fun KtCallExpression.parseStroke(): IrStroke? { + val arg = valueArguments.find { it.getArgumentName()?.asName?.identifier == "stroke" } + val colorCall = arg?.getArgumentExpression().safeAs() + + return when (val color = colorCall?.parseColor()) { + null -> null + else -> IrStroke.Color(color.colorHex) + } +} + +internal fun KtCallExpression.parseColor(): IrFill.Color? { + val colorArg = valueArguments.firstOrNull()?.getArgumentExpression()?.text + ?.removePrefix("Color(0x") + ?.removePrefix("0x") + ?.removeSuffix(")") ?: return null + return IrFill.Color(colorHex = colorArg) +} diff --git a/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/PathNodeParser.kt b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/PathNodeParser.kt new file mode 100644 index 00000000..611e73d4 --- /dev/null +++ b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/common/PathNodeParser.kt @@ -0,0 +1,167 @@ +package io.github.composegears.valkyrie.psi.imagevector.common + +import io.github.composegears.valkyrie.ir.IrPathNode +import io.github.composegears.valkyrie.ir.IrPathNode.ArcTo +import io.github.composegears.valkyrie.ir.IrPathNode.Close +import io.github.composegears.valkyrie.ir.IrPathNode.CurveTo +import io.github.composegears.valkyrie.ir.IrPathNode.HorizontalTo +import io.github.composegears.valkyrie.ir.IrPathNode.LineTo +import io.github.composegears.valkyrie.ir.IrPathNode.MoveTo +import io.github.composegears.valkyrie.ir.IrPathNode.QuadTo +import io.github.composegears.valkyrie.ir.IrPathNode.ReflectiveCurveTo +import io.github.composegears.valkyrie.ir.IrPathNode.ReflectiveQuadTo +import io.github.composegears.valkyrie.ir.IrPathNode.RelativeArcTo +import io.github.composegears.valkyrie.ir.IrPathNode.RelativeCurveTo +import io.github.composegears.valkyrie.ir.IrPathNode.RelativeHorizontalTo +import io.github.composegears.valkyrie.ir.IrPathNode.RelativeLineTo +import io.github.composegears.valkyrie.ir.IrPathNode.RelativeMoveTo +import io.github.composegears.valkyrie.ir.IrPathNode.RelativeQuadTo +import io.github.composegears.valkyrie.ir.IrPathNode.RelativeReflectiveCurveTo +import io.github.composegears.valkyrie.ir.IrPathNode.RelativeReflectiveQuadTo +import io.github.composegears.valkyrie.ir.IrPathNode.RelativeVerticalTo +import io.github.composegears.valkyrie.ir.IrPathNode.VerticalTo +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtCallExpression + +fun KtBlockExpression.parsePathNodes(): List { + val pathNodes = mutableListOf() + + statements.filterIsInstance().forEach { expression -> + val args = expression.valueArguments.mapNotNull { it.getArgumentExpression()?.text } + when (expression.calleeExpression?.text) { + "close" -> pathNodes += Close + "moveToRelative" -> if (args.size == 2 && args.allFloat()) { + pathNodes += RelativeMoveTo( + x = args[0].toFloat(), + y = args[1].toFloat(), + ) + } + "moveTo" -> if (args.size == 2 && args.allFloat()) { + pathNodes += MoveTo( + x = args[0].toFloat(), + y = args[1].toFloat(), + ) + } + "lineToRelative" -> if (args.size == 2 && args.allFloat()) { + pathNodes += RelativeLineTo( + x = args[0].toFloat(), + y = args[1].toFloat(), + ) + } + "lineTo" -> if (args.size == 2 && args.allFloat()) { + pathNodes += LineTo( + x = args[0].toFloat(), + y = args[1].toFloat(), + ) + } + "horizontalLineToRelative" -> if (args.size == 1 && args.allFloat()) { + pathNodes += RelativeHorizontalTo(x = args[0].toFloat()) + } + "horizontalLineTo" -> if (args.size == 1 && args.allFloat()) { + pathNodes += HorizontalTo(x = args[0].toFloat()) + } + "verticalLineToRelative" -> if (args.size == 1 && args.allFloat()) { + pathNodes += RelativeVerticalTo(y = args[0].toFloat()) + } + "verticalLineTo" -> if (args.size == 1 && args.allFloat()) { + pathNodes += VerticalTo(y = args[0].toFloat()) + } + "curveToRelative" -> if (args.size == 6 && args.allFloat()) { + pathNodes += RelativeCurveTo( + dx1 = args[0].toFloat(), + dy1 = args[1].toFloat(), + dx2 = args[2].toFloat(), + dy2 = args[3].toFloat(), + dx3 = args[4].toFloat(), + dy3 = args[5].toFloat(), + ) + } + "curveTo" -> if (args.size == 6 && args.allFloat()) { + pathNodes += CurveTo( + x1 = args[0].toFloat(), + y1 = args[1].toFloat(), + x2 = args[2].toFloat(), + y2 = args[3].toFloat(), + x3 = args[4].toFloat(), + y3 = args[5].toFloat(), + ) + } + "reflectiveCurveToRelative" -> if (args.size == 4 && args.allFloat()) { + pathNodes += RelativeReflectiveCurveTo( + x1 = args[0].toFloat(), + y1 = args[1].toFloat(), + x2 = args[2].toFloat(), + y2 = args[3].toFloat(), + ) + } + "reflectiveCurveTo" -> if (args.size == 4 && args.allFloat()) { + pathNodes += ReflectiveCurveTo( + x1 = args[0].toFloat(), + y1 = args[1].toFloat(), + x2 = args[2].toFloat(), + y2 = args[3].toFloat(), + ) + } + "quadToRelative" -> if (args.size == 4 && args.allFloat()) { + pathNodes += RelativeQuadTo( + x1 = args[0].toFloat(), + y1 = args[1].toFloat(), + x2 = args[2].toFloat(), + y2 = args[3].toFloat(), + ) + } + "quadTo" -> if (args.size == 4 && args.allFloat()) { + pathNodes += QuadTo( + x1 = args[0].toFloat(), + y1 = args[1].toFloat(), + x2 = args[2].toFloat(), + y2 = args[3].toFloat(), + ) + } + "reflectiveQuadToRelative" -> if (args.size == 2 && args.allFloat()) { + pathNodes += RelativeReflectiveQuadTo( + x = args[0].toFloat(), + y = args[1].toFloat(), + ) + } + "reflectiveQuadTo" -> if (args.size == 2 && args.allFloat()) { + pathNodes += ReflectiveQuadTo( + x = args[0].toFloat(), + y = args[1].toFloat(), + ) + } + "arcToRelative" -> if (args.size == 7 && args.allFloatOrBoolean()) { + pathNodes += RelativeArcTo( + horizontalEllipseRadius = args[0].toFloat(), + verticalEllipseRadius = args[1].toFloat(), + theta = args[2].toFloat(), + isMoreThanHalf = args[3].toBoolean(), + isPositiveArc = args[4].toBoolean(), + arcStartDx = args[5].toFloat(), + arcStartDy = args[6].toFloat(), + ) + } + "arcTo" -> if (args.size == 7 && args.allFloatOrBoolean()) { + pathNodes += ArcTo( + horizontalEllipseRadius = args[0].toFloat(), + verticalEllipseRadius = args[1].toFloat(), + theta = args[2].toFloat(), + isMoreThanHalf = args[3].toBoolean(), + isPositiveArc = args[4].toBoolean(), + arcStartX = args[5].toFloat(), + arcStartY = args[6].toFloat(), + ) + } + } + } + + return pathNodes +} + +private fun List.allFloat() = all(String::isFloat) +private fun List.allFloatOrBoolean() = all(String::isFloatOrBoolean) + +private fun String.isFloat() = toFloatOrNull() != null + +private fun String.isFloatOrBoolean() = + isFloat() || this == "true" || this == "false" || this == "0" || this == "1" diff --git a/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/parser/MaterialImageVectorPsiParser.kt b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/parser/MaterialImageVectorPsiParser.kt new file mode 100644 index 00000000..3f0ba698 --- /dev/null +++ b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/parser/MaterialImageVectorPsiParser.kt @@ -0,0 +1,90 @@ +package io.github.composegears.valkyrie.psi.imagevector.parser + +import androidx.compose.ui.graphics.vector.ImageVector +import io.github.composegears.valkyrie.extensions.safeAs +import io.github.composegears.valkyrie.ir.IrFill +import io.github.composegears.valkyrie.ir.IrImageVector +import io.github.composegears.valkyrie.ir.IrStrokeLineJoin +import io.github.composegears.valkyrie.ir.IrVectorNode +import io.github.composegears.valkyrie.ir.IrVectorNode.IrPath +import io.github.composegears.valkyrie.psi.extension.childOfType +import io.github.composegears.valkyrie.psi.extension.childrenOfType +import io.github.composegears.valkyrie.psi.imagevector.common.extractPathFillType +import io.github.composegears.valkyrie.psi.imagevector.common.parsePathNodes +import io.github.composegears.valkyrie.psi.imagevector.common.toComposeImageVector +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtStringTemplateExpression + +internal object MaterialImageVectorPsiParser { + + fun parse(ktFile: KtFile): ImageVector? { + val property = ktFile.childOfType() ?: return null + val blockBody = property.getter?.bodyBlockExpression ?: return null + + val materialIconCall = blockBody.childrenOfType().firstOrNull { + it.calleeExpression?.text == "materialIcon" + } ?: return null + + return IrImageVector( + name = materialIconCall.extractIconName(), + defaultWidth = 24f, + defaultHeight = 24f, + viewportWidth = 24f, + viewportHeight = 24f, + autoMirror = materialIconCall.extractAutoMirror(), + vectorNodes = blockBody.parseMaterialPath(), + ).toComposeImageVector() + } + + private fun KtCallExpression.extractIconName(): String { + val nameArgument = valueArguments.find { arg -> + arg?.getArgumentName()?.asName?.identifier == "name" + } + + return nameArgument?.getArgumentExpression().safeAs() + ?.entries + ?.firstOrNull() + ?.text.orEmpty() + } + + private fun KtCallExpression.extractAutoMirror(): Boolean { + val autoMirrorArgument = valueArguments.find { arg -> + arg?.getArgumentName()?.asName?.identifier == "autoMirror" + } + + return autoMirrorArgument?.getArgumentExpression()?.text?.toBoolean() ?: false + } + + private fun KtBlockExpression.parseMaterialPath(): List { + val materialPathCall = childrenOfType().firstOrNull { + it.calleeExpression?.text == "materialPath" + } ?: return emptyList() + + val pathLambda = materialPathCall.lambdaArguments.firstOrNull()?.getLambdaExpression() + val pathBody = pathLambda?.bodyExpression ?: return emptyList() + + return listOf( + IrPath( + fill = IrFill.Color(colorHex = "FF000000"), + fillAlpha = materialPathCall.extractFloat("fillAlpha", 1f), + strokeAlpha = materialPathCall.extractFloat("strokeAlpha", 1f), + strokeLineWidth = 1f, + strokeLineJoin = IrStrokeLineJoin.Bevel, + strokeLineMiter = 1f, + pathFillType = materialPathCall.extractPathFillType(), + nodes = pathBody.parsePathNodes(), + ), + ) + } + + private fun KtCallExpression.extractFloat(argName: String, defaultValue: Float): Float { + val argument = valueArguments.find { arg -> + arg?.getArgumentName()?.asName?.identifier == argName + } + + return argument?.getArgumentExpression()?.text?.toFloatOrNull() ?: defaultValue + } +} diff --git a/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/parser/RegularImageVectorPsiParser.kt b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/parser/RegularImageVectorPsiParser.kt new file mode 100644 index 00000000..ea00de2e --- /dev/null +++ b/components/psi/imagevector/src/main/kotlin/io/github/composegears/valkyrie/psi/imagevector/parser/RegularImageVectorPsiParser.kt @@ -0,0 +1,132 @@ +package io.github.composegears.valkyrie.psi.imagevector.parser + +import androidx.compose.ui.graphics.vector.ImageVector +import io.github.composegears.valkyrie.ir.IrImageVector +import io.github.composegears.valkyrie.ir.IrVectorNode +import io.github.composegears.valkyrie.psi.extension.childOfType +import io.github.composegears.valkyrie.psi.extension.childrenOfType +import io.github.composegears.valkyrie.psi.imagevector.common.extractPathFillType +import io.github.composegears.valkyrie.psi.imagevector.common.extractStrokeCap +import io.github.composegears.valkyrie.psi.imagevector.common.extractStrokeJoin +import io.github.composegears.valkyrie.psi.imagevector.common.parseFill +import io.github.composegears.valkyrie.psi.imagevector.common.parseFloatArg +import io.github.composegears.valkyrie.psi.imagevector.common.parsePathNodes +import io.github.composegears.valkyrie.psi.imagevector.common.parseStroke +import io.github.composegears.valkyrie.psi.imagevector.common.toComposeImageVector +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtProperty + +internal object RegularImageVectorPsiParser { + + fun parse(ktFile: KtFile): ImageVector? { + val property = ktFile.childOfType() ?: return null + + val blockBody = property.getter?.bodyBlockExpression + ?: property.delegateExpression?.childrenOfType()?.firstOrNull() + ?: return null + + val ktImageVector = blockBody.parseImageVectorParams() ?: return null + + return IrImageVector( + name = ktImageVector.name.ifEmpty { property.name.orEmpty() }, + defaultWidth = ktImageVector.defaultWidth, + defaultHeight = ktImageVector.defaultHeight, + viewportWidth = ktImageVector.viewportWidth, + viewportHeight = ktImageVector.viewportHeight, + vectorNodes = blockBody.parseApplyBlock(), + ).toComposeImageVector() + } + + private fun KtBlockExpression.parseImageVectorParams(): IrImageVector? { + val imageVectorBuilderCall = childrenOfType().firstOrNull { + it.calleeExpression?.text == "Builder" + } ?: return null + + var name = "" + var defaultWidth = 0f + var defaultHeight = 0f + var viewportWidth = 0f + var viewportHeight = 0f + + imageVectorBuilderCall.valueArguments + .forEach { arg -> + val argName = arg.getArgumentName()?.asName?.identifier + val argValue = arg.getArgumentExpression()?.text + + when (argName) { + "name" -> name = argValue?.removeSurrounding("\"").orEmpty() + "defaultWidth" -> defaultWidth = parseValue(argValue) + "defaultHeight" -> defaultHeight = parseValue(argValue) + "viewportWidth" -> viewportWidth = parseValue(argValue) + "viewportHeight" -> viewportHeight = parseValue(argValue) + } + } + + return IrImageVector( + name = name, + defaultWidth = defaultWidth, + defaultHeight = defaultHeight, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + vectorNodes = emptyList(), + ) + } + + private fun parseValue(value: String?): Float { + if (value == null) return 0f + + return value + .replace("dp", "") + .replace("f", "") + .toFloatOrNull() ?: 0f + } + + private fun KtBlockExpression.parseApplyBlock(): List { + val vectorNodes = mutableListOf() + + val applyFunctionCall = childrenOfType() + .firstOrNull { it.calleeExpression?.text == "apply" } + + val lambdaExpression = applyFunctionCall?.lambdaArguments?.firstOrNull()?.getLambdaExpression() + val applyBlock = lambdaExpression?.bodyExpression ?: return vectorNodes + + applyBlock.statements.filterIsInstance().forEach { expression -> + if (expression.calleeExpression?.text == "path") { + vectorNodes += expression.parsePath() + } + if (expression.calleeExpression?.text == "group") { + val groupLambda = expression.lambdaArguments.firstOrNull()?.getLambdaExpression() + val groupBlock = groupLambda?.bodyExpression + + vectorNodes += IrVectorNode.IrGroup( + nodes = groupBlock?.statements + ?.filterIsInstance() + ?.map { it.parsePath() } + .orEmpty(), + ) + } + } + + return vectorNodes + } + + private fun KtCallExpression.parsePath(): IrVectorNode.IrPath { + val pathLambda = lambdaArguments.firstOrNull()?.getLambdaExpression() + val pathBody = pathLambda?.bodyExpression + + return IrVectorNode.IrPath( + fill = parseFill(), + fillAlpha = parseFloatArg("fillAlpha") ?: 1f, + stroke = parseStroke(), + strokeAlpha = parseFloatArg("strokeAlpha") ?: 1f, + strokeLineWidth = parseFloatArg("strokeLineWidth") ?: 0f, + strokeLineCap = extractStrokeCap(), + strokeLineJoin = extractStrokeJoin(), + strokeLineMiter = parseFloatArg("strokeLineMiter") ?: 4f, + pathFillType = extractPathFillType(), + nodes = pathBody?.parsePathNodes().orEmpty(), + ) + } +} diff --git a/components/psi/imagevector/src/test/kotlin/io/github/composegears/valkyrie/psi/imagevector/ImageVectorPsiParserTest.kt b/components/psi/imagevector/src/test/kotlin/io/github/composegears/valkyrie/psi/imagevector/ImageVectorPsiParserTest.kt new file mode 100644 index 00000000..25f67f55 --- /dev/null +++ b/components/psi/imagevector/src/test/kotlin/io/github/composegears/valkyrie/psi/imagevector/ImageVectorPsiParserTest.kt @@ -0,0 +1,289 @@ +package io.github.composegears.valkyrie.psi.imagevector + +import androidx.compose.material.icons.materialIcon +import androidx.compose.material.icons.materialPath +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiManager +import com.intellij.testFramework.LightVirtualFile +import com.intellij.testFramework.ProjectExtension +import io.github.composegears.valkyrie.extensions.ResourceUtils.getResourceText +import io.github.composegears.valkyrie.extensions.cast +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.psi.KtFile +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@Suppress("UnstableApiUsage") +class ImageVectorPsiParserTest { + + companion object { + @RegisterExtension + val projectExtension = ProjectExtension() + } + + private val project: Project + get() = projectExtension.project + + private fun createKtFile(from: String): KtFile { + return PsiManager.getInstance(project) + .findFile(LightVirtualFile("", KotlinFileType.INSTANCE, getResourceText(from))) + .cast() + } + + @Test + fun `empty image vector`() = invokeAndWaitIfNeeded { + val ktFile = createKtFile(from = "EmptyImageVector.kt") + val imageVector = ImageVectorPsiParser.parseToImageVector(ktFile) + + val expected = ImageVector.Builder( + name = "EmptyImageVector", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 18f, + viewportHeight = 18f, + ).build() + + assertThat(imageVector).isEqualTo(expected) + } + + @Test + fun `empty paths`() = invokeAndWaitIfNeeded { + val ktFile = createKtFile(from = "EmptyPaths.kt") + val imageVector = ImageVectorPsiParser.parseToImageVector(ktFile) + + val expected = ImageVector.Builder( + name = "EmptyPaths", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 18f, + viewportHeight = 18f, + ).apply { + path { } + path { } + path { } + }.build() + + assertThat(imageVector).isEqualTo(expected) + } + + @Test + fun `parse all path params`() = invokeAndWaitIfNeeded { + val ktFile = createKtFile(from = "AllPathParams.kt") + val imageVector = ImageVectorPsiParser.parseToImageVector(ktFile) + + val expected = ImageVector.Builder( + name = "AllPathParams", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 18f, + viewportHeight = 18f, + ).apply { + path( + fill = SolidColor(Color(0xFF232F34)), + fillAlpha = 0.5f, + stroke = SolidColor(Color(0xFF232F34)), + strokeAlpha = 0.5f, + strokeLineWidth = 1f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + strokeLineMiter = 3f, + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(6.75f, 12.127f) + moveToRelative(1f, -2f) + lineTo(3.623f, 9f) + lineToRelative(-5.49f, 1.3f) + horizontalLineTo(1.4f) + horizontalLineToRelative(-6f) + verticalLineTo(95.06f) + verticalLineToRelative(10.0f) + curveTo(11.76f, 1.714f, 11.755f, 1.715f, 11.768f, 1.714f) + curveToRelative(3.236f, 0.224f, 7.033f, 0f, 7.033f, 0f) + reflectiveCurveTo(11.957f, 41.979f, 0.013f, 44.716f) + reflectiveCurveToRelative(6.586f, 6.584f, 9.823f, 6.805f) + quadTo(20.306f, 6.477f, 20.306f, 6.508f) + quadToRelative(0.04f, -0.3f, 0.06f, -0.61f) + reflectiveQuadTo(5f, 3f) + reflectiveQuadToRelative(4f, 1f) + arcTo(0.75f, 0.75f, 0f, isMoreThanHalf = false, isPositiveArc = false, 3f, 5.092f) + arcTo(0.75f, 0.75f, 0f, true, false, 3f, 5.092f) + arcToRelative(0.763f, 0.763f, 0f, isMoreThanHalf = false, isPositiveArc = true, -0.55f, -0.066f) + arcToRelative(0.763f, 0.763f, 0f, false, true, -0.55f, -0.066f) + close() + } + }.build() + + assertThat(imageVector).isEqualTo(expected) + } + + @Test + fun `parse material icon`() = invokeAndWaitIfNeeded { + val ktFile = createKtFile(from = "MaterialIcon.kt") + val imageVector = ImageVectorPsiParser.parseToImageVector(ktFile) + + val expected = materialIcon(name = "Filled.Settings", autoMirror = true) { + materialPath( + fillAlpha = 0.5f, + strokeAlpha = 0.6f, + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(19.14f, 12.94f) + close() + } + } + + assertThat(imageVector).isEqualTo(expected) + } + + @Test + fun `parse icon with group`() = invokeAndWaitIfNeeded { + val ktFile = createKtFile(from = "IconWithGroup.kt") + val imageVector = ImageVectorPsiParser.parseToImageVector(ktFile) + + val expected = ImageVector.Builder( + name = "IconWithGroup", + defaultWidth = 48.dp, + defaultHeight = 48.dp, + viewportWidth = 512f, + viewportHeight = 512f, + ).apply { + group { + path(fill = SolidColor(Color(0xFFD80027))) { + moveTo(0f, 0f) + horizontalLineToRelative(512f) + verticalLineToRelative(167f) + lineToRelative(-23.2f, 89.7f) + lineTo(512f, 345f) + verticalLineToRelative(167f) + horizontalLineTo(0f) + verticalLineTo(345f) + lineToRelative(29.4f, -89f) + lineTo(0f, 167f) + close() + } + path(fill = SolidColor(Color(0xFFEEEEEE))) { + moveTo(0f, 167f) + horizontalLineToRelative(512f) + verticalLineToRelative(178f) + horizontalLineTo(0f) + close() + } + } + }.build() + + assertThat(imageVector).isEqualTo(expected) + } + + @Test + fun `parse icon with linear and radial gradient`() = invokeAndWaitIfNeeded { + val ktFile = createKtFile(from = "IconWithGradient.kt") + val imageVector = ImageVectorPsiParser.parseToImageVector(ktFile) + + val expected = ImageVector.Builder( + name = "IconWithGradient", + defaultWidth = 51.dp, + defaultHeight = 63.dp, + viewportWidth = 51f, + viewportHeight = 63f, + ).apply { + path( + fill = Brush.radialGradient( + colorStops = arrayOf( + 0.19f to Color(0xFFD53A42), + 0.39f to Color(0xFFDF7A40), + 0.59f to Color(0xFFF0A941), + 1f to Color(0xFFFFFFF0), + ), + center = Offset(0f, 10f), + radius = 100f, + ), + stroke = SolidColor(Color(0x00000000)), + ) { + moveTo(0f, 0f) + horizontalLineToRelative(100f) + verticalLineToRelative(20f) + horizontalLineToRelative(-100f) + close() + } + path( + fill = Brush.linearGradient( + colorStops = arrayOf( + 0.126f to Color(0xFFE7BD76), + 0.13f to Color(0xFFE6BB74), + 0.247f to Color(0xFFD48E4E), + 0.334f to Color(0xFFCC753B), + 0.38f to Color(0xFFC96C35), + 0.891f to Color(0xFFC96D34), + 0.908f to Color(0xFFCC7439), + 0.937f to Color(0xFFD28647), + 0.973f to Color(0xFFDEA763), + 0.989f to Color(0xFFE5B972), + ), + start = Offset(46.778f, 40.493f), + end = Offset(24.105f, 63.166f), + ), + ) { + moveTo(51f, 44.716f) + reflectiveCurveToRelative(-6.586f, 6.584f, -9.823f, 6.805f) + curveToRelative(-3.235f, 0.224f, -7.032f, 0f, -7.032f, 0f) + reflectiveCurveToRelative(-7.024f, 1.732f, -7.024f, 7.368f) + verticalLineTo(63f) + lineToRelative(-3.195f, -0.014f) + verticalLineToRelative(-5.571f) + curveToRelative(0f, -6.857f, 10.052f, -7.567f, 10.052f, -7.567f) + reflectiveCurveTo(39.057f, 41.979f, 51f, 44.716f) + } + }.build() + + // assertKt error due usage arrayList inside ImageVector: + // "did not compare equal to the same type with the same string representation" + assertThat(imageVector.toString()).isEqualTo(expected.toString()) + } + + @Test + fun `parse lazy property`() = invokeAndWaitIfNeeded { + val ktFile = createKtFile(from = "LazyProperty.kt") + val imageVector = ImageVectorPsiParser.parseToImageVector(ktFile) + + val expected = ImageVector.Builder( + name = "LazyProperty", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(Color(0xFF232F34))) { + moveTo(19f, 13f) + lineTo(13f, 13f) + lineTo(13f, 19f) + lineTo(11f, 19f) + lineTo(11f, 13f) + lineTo(5f, 13f) + lineTo(5f, 11f) + lineTo(11f, 11f) + lineTo(11f, 5f) + lineTo(13f, 5f) + lineTo(13f, 11f) + lineTo(19f, 11f) + lineTo(19f, 13f) + close() + } + }.build() + + assertThat(imageVector).isEqualTo(expected) + } +} diff --git a/components/psi/imagevector/src/test/resources/AllPathParams.kt b/components/psi/imagevector/src/test/resources/AllPathParams.kt new file mode 100644 index 00000000..11761c83 --- /dev/null +++ b/components/psi/imagevector/src/test/resources/AllPathParams.kt @@ -0,0 +1,64 @@ +package io.github.composegears.valkyrie.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import kotlin.Suppress + +val ValkyrieIcons.AllPathParams: ImageVector + get() { + if (_AllPathParams != null) { + return _AllPathParams!! + } + _AllPathParams = ImageVector.Builder( + name = "AllPathParams", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 18f, + viewportHeight = 18f + ).apply { + path( + fill = SolidColor(Color(0xFF232F34)), + fillAlpha = 0.5f, + stroke = SolidColor(Color(0xFF232F34)), + strokeAlpha = 0.5f, + strokeLineWidth = 1f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + strokeLineMiter = 3f, + pathFillType = PathFillType.EvenOdd + ) { + moveTo(6.75f, 12.127f) + moveToRelative(1f, -2f) + lineTo(3.623f, 9f) + lineToRelative(-5.49f, 1.3f) + horizontalLineTo(1.4f) + horizontalLineToRelative(-6f) + verticalLineTo(95.06f) + verticalLineToRelative(10.0f) + curveTo(11.76f, 1.714f, 11.755f, 1.715f, 11.768f, 1.714f) + curveToRelative(3.236f, 0.224f, 7.033f, 0f, 7.033f, 0f) + reflectiveCurveTo(11.957f, 41.979f, 0.013f, 44.716f) + reflectiveCurveToRelative(6.586f, 6.584f, 9.823f, 6.805f) + quadTo(20.306f, 6.477f, 20.306f, 6.508f) + quadToRelative(0.04f, -0.3f, 0.06f, -0.61f) + reflectiveQuadTo(5f, 3f) + reflectiveQuadToRelative(4f, 1f) + arcTo(0.75f, 0.75f, 0f, isMoreThanHalf = false, isPositiveArc = false, 3f, 5.092f) + arcTo(0.75f, 0.75f, 0f, true, false, 3f, 5.092f) + arcToRelative(0.763f, 0.763f, 0f, isMoreThanHalf = false, isPositiveArc = true, -0.55f, -0.066f) + arcToRelative(0.763f, 0.763f, 0f, false, true, -0.55f, -0.066f) + close() + } + }.build() + + return _AllPathParams!! + } + +@Suppress("ObjectPropertyName") +private var _AllPathParams: ImageVector? = null diff --git a/components/psi/imagevector/src/test/resources/EmptyImageVector.kt b/components/psi/imagevector/src/test/resources/EmptyImageVector.kt new file mode 100644 index 00000000..aa067574 --- /dev/null +++ b/components/psi/imagevector/src/test/resources/EmptyImageVector.kt @@ -0,0 +1,23 @@ +package io.github.composegears.valkyrie.icons + +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +val ValkyrieIcons.EmptyImageVector: ImageVector + get() { + if (_EmptyImageVector != null) { + return _EmptyImageVector!! + } + _EmptyImageVector = ImageVector.Builder( + name = "EmptyImageVector", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 18f, + viewportHeight = 18f, + ).build() + + return _EmptyImageVector!! + } + +@Suppress("ObjectPropertyName") +private var _EmptyImageVector: ImageVector? = null diff --git a/components/psi/imagevector/src/test/resources/EmptyPaths.kt b/components/psi/imagevector/src/test/resources/EmptyPaths.kt new file mode 100644 index 00000000..7107bc0e --- /dev/null +++ b/components/psi/imagevector/src/test/resources/EmptyPaths.kt @@ -0,0 +1,28 @@ +package io.github.composegears.valkyrie.icons + +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val ValkyrieIcons.EmptyPaths: ImageVector + get() { + if (_EmptyPaths != null) { + return _EmptyPaths!! + } + _EmptyPaths = ImageVector.Builder( + name = "EmptyPaths", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 18f, + viewportHeight = 18f, + ).apply { + path { } + path { } + path { } + }.build() + + return _EmptyPaths!! + } + +@Suppress("ObjectPropertyName") +private var _EmptyPaths: ImageVector? = null diff --git a/components/psi/imagevector/src/test/resources/IconWithGradient.kt b/components/psi/imagevector/src/test/resources/IconWithGradient.kt new file mode 100644 index 00000000..7e1fa0eb --- /dev/null +++ b/components/psi/imagevector/src/test/resources/IconWithGradient.kt @@ -0,0 +1,74 @@ +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val IconWithGradient: ImageVector + get() { + if (_IconWithGradient != null) { + return _IconWithGradient!! + } + _IconWithGradient = ImageVector.Builder( + name = "IconWithGradient", + defaultWidth = 51.dp, + defaultHeight = 63.dp, + viewportWidth = 51f, + viewportHeight = 63f, + ).apply { + path( + fill = Brush.radialGradient( + colorStops = arrayOf( + 0.19f to Color(0xFFD53A42), + 0.39f to Color(0xFFDF7A40), + 0.59f to Color(0xFFF0A941), + 1f to Color(0xFFFFFFF0), + ), + center = Offset(0f, 10f), + radius = 100f, + ), + stroke = SolidColor(Color(0x00000000)), + ) { + moveTo(0f, 0f) + horizontalLineToRelative(100f) + verticalLineToRelative(20f) + horizontalLineToRelative(-100f) + close() + } + path( + fill = Brush.linearGradient( + colorStops = arrayOf( + 0.126f to Color(0xFFE7BD76), + 0.13f to Color(0xFFE6BB74), + 0.247f to Color(0xFFD48E4E), + 0.334f to Color(0xFFCC753B), + 0.38f to Color(0xFFC96C35), + 0.891f to Color(0xFFC96D34), + 0.908f to Color(0xFFCC7439), + 0.937f to Color(0xFFD28647), + 0.973f to Color(0xFFDEA763), + 0.989f to Color(0xFFE5B972), + ), + start = Offset(46.778f, 40.493f), + end = Offset(24.105f, 63.166f), + ), + ) { + moveTo(51f, 44.716f) + reflectiveCurveToRelative(-6.586f, 6.584f, -9.823f, 6.805f) + curveToRelative(-3.235f, 0.224f, -7.032f, 0f, -7.032f, 0f) + reflectiveCurveToRelative(-7.024f, 1.732f, -7.024f, 7.368f) + verticalLineTo(63f) + lineToRelative(-3.195f, -0.014f) + verticalLineToRelative(-5.571f) + curveToRelative(0f, -6.857f, 10.052f, -7.567f, 10.052f, -7.567f) + reflectiveCurveTo(39.057f, 41.979f, 51f, 44.716f) + } + }.build() + + return _IconWithGradient!! + } + +@Suppress("ObjectPropertyName") +private var _IconWithGradient: ImageVector? = null diff --git a/components/psi/imagevector/src/test/resources/IconWithGroup.kt b/components/psi/imagevector/src/test/resources/IconWithGroup.kt new file mode 100644 index 00000000..bfc9b8a1 --- /dev/null +++ b/components/psi/imagevector/src/test/resources/IconWithGroup.kt @@ -0,0 +1,41 @@ +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +val ValkyrieIcons.IconWithGroup: ImageVector + get() { + if (_IconWithGroup != null) { + return _IconWithGroup!! + } + _IconWithGroup = ImageVector.Builder( + name = "IconWithGroup", + defaultWidth = 48.dp, + defaultHeight = 48.dp, + viewportWidth = 512f, + viewportHeight = 512f, + ).apply { + group { + path(fill = SolidColor(Color(0xFFD80027))) { + moveTo(0f, 0f) + horizontalLineToRelative(512f) + verticalLineToRelative(167f) + lineToRelative(-23.2f, 89.7f) + lineTo(512f, 345f) + verticalLineToRelative(167f) + horizontalLineTo(0f) + verticalLineTo(345f) + lineToRelative(29.4f, -89f) + lineTo(0f, 167f) + close() + } + path(fill = SolidColor(Color(0xFFEEEEEE))) { + moveTo(0f, 167f) + horizontalLineToRelative(512f) + verticalLineToRelative(178f) + horizontalLineTo(0f) + close() + } + } + }.build() + + return _IconWithGroup!! + } diff --git a/components/psi/imagevector/src/test/resources/LazyProperty.kt b/components/psi/imagevector/src/test/resources/LazyProperty.kt new file mode 100644 index 00000000..7704eba6 --- /dev/null +++ b/components/psi/imagevector/src/test/resources/LazyProperty.kt @@ -0,0 +1,32 @@ +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val Outlined.LazyProperty: ImageVector by lazy(LazyThreadSafetyMode.NONE) { + ImageVector.Builder( + name = "LazyProperty", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path(fill = SolidColor(Color(0xFF232F34))) { + moveTo(19f, 13f) + lineTo(13f, 13f) + lineTo(13f, 19f) + lineTo(11f, 19f) + lineTo(11f, 13f) + lineTo(5f, 13f) + lineTo(5f, 11f) + lineTo(11f, 11f) + lineTo(11f, 5f) + lineTo(13f, 5f) + lineTo(13f, 11f) + lineTo(19f, 11f) + lineTo(19f, 13f) + close() + } + }.build() +} diff --git a/components/psi/imagevector/src/test/resources/MaterialIcon.kt b/components/psi/imagevector/src/test/resources/MaterialIcon.kt new file mode 100644 index 00000000..a20f53d7 --- /dev/null +++ b/components/psi/imagevector/src/test/resources/MaterialIcon.kt @@ -0,0 +1,28 @@ +package androidx.compose.material.icons.filled + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.materialIcon +import androidx.compose.material.icons.materialPath +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.Path + +public val Icons.Filled.Settings: ImageVector + get() { + if (_settings != null) { + return _settings!! + } + _settings = materialIcon(name = "Filled.Settings", autoMirror = true) { + materialPath( + fillAlpha = 0.5f, + strokeAlpha = 0.6f, + pathFillType = PathFillType.EvenOdd + ) { + moveTo(19.14f, 12.94f) + close() + } + } + return _settings!! + } + +private var _settings: ImageVector? = null diff --git a/idea-plugin/build.gradle.kts b/idea-plugin/build.gradle.kts index 6340680f..97e06e3c 100644 --- a/idea-plugin/build.gradle.kts +++ b/idea-plugin/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(projects.components.generator.imagevector) implementation(projects.components.parser) implementation(projects.components.psi.iconpack) + implementation(projects.components.psi.imagevector) compileOnly(compose.desktop.currentOs) implementation(compose.desktop.common) diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ImageVectorPreviewEditorProvider.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ImageVectorPreviewEditorProvider.kt new file mode 100644 index 00000000..842b49ac --- /dev/null +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ImageVectorPreviewEditorProvider.kt @@ -0,0 +1,40 @@ +package io.github.composegears.valkyrie.editor + +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.FileEditorPolicy +import com.intellij.openapi.fileEditor.FileEditorProvider +import com.intellij.openapi.fileEditor.impl.text.QuickDefinitionProvider +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile + +class ImageVectorPreviewEditorProvider : + FileEditorProvider, + QuickDefinitionProvider, + DumbAware { + override fun getEditorTypeId(): String = "ImageVectorPreviewEditorProvider" + + override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.HIDE_DEFAULT_EDITOR + + override fun accept(project: Project, file: VirtualFile): Boolean { + if (file.extension != "kt") return false + + val content = file.inputStream + .bufferedReader() + .use { it.readText() } + + return content.contains("androidx.compose.ui.graphics.vector.ImageVector") && + ( + content.contains("androidx.compose.ui.graphics.vector.path") || + content.contains("androidx.compose.ui.graphics.vector.group") || + content.contains("androidx.compose.material.icons.materialIcon") || + content.contains("androidx.compose.material.icons.materialPath") + ) + } + + override fun acceptRequiresReadAction(): Boolean = false + + override fun createEditor(project: Project, file: VirtualFile): FileEditor { + return TextEditorWithImageVectorPreview(project, file) + } +} diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/TextEditorWithImageVectorPreview.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/TextEditorWithImageVectorPreview.kt new file mode 100644 index 00000000..4664ed37 --- /dev/null +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/TextEditorWithImageVectorPreview.kt @@ -0,0 +1,126 @@ +package io.github.composegears.valkyrie.editor + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposePanel +import androidx.compose.ui.unit.dp +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.FileEditorState +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.fileEditor.TextEditorWithPreview +import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiFileFactory +import com.intellij.util.ui.JBUI +import io.github.composegears.valkyrie.psi.imagevector.ImageVectorPsiParser +import io.github.composegears.valkyrie.ui.foundation.PixelGrid +import io.github.composegears.valkyrie.ui.foundation.theme.ValkyrieTheme +import java.awt.Dimension +import javax.swing.JComponent +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.psi.KtFile + +class TextEditorWithImageVectorPreview( + project: Project, + file: VirtualFile, +) : TextEditorWithPreview( + /* editor = */ + createTextEditor(project, file), + /* preview = */ + createPreviewEditor(project, file), +) { + companion object { + private fun createTextEditor(project: Project, file: VirtualFile): TextEditor { + return TextEditorProvider.getInstance().createEditor(project, file) as TextEditor + } + + private fun createPreviewEditor(project: Project, file: VirtualFile): FileEditor { + return ImageVectorPreviewEditor(project, file) + } + } +} + +private class ImageVectorPreviewEditor( + private val project: Project, + private val file: VirtualFile, +) : FileEditor { + + init { + System.setProperty("compose.swing.render.on.graphics", "true") + System.setProperty("compose.interop.blending", "true") + } + + private val composePanel = ComposePanel().apply { + setContent { + ValkyrieTheme(project, this) { + Surface(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + val imageVector = remember(file) { + val ktFile = file.toKtFile(project) + if (ktFile == null) { + null + } else { + ImageVectorPsiParser.parseToImageVector(ktFile) + } + } + + if (imageVector == null) { + Text("Failed to parse to ImageVector, please submit issue") + } else { + Box(modifier = Modifier.size(200.dp)) { + PixelGrid( + modifier = Modifier.matchParentSize(), + gridSize = 2.dp, + ) + Image( + modifier = Modifier.size(200.dp), + imageVector = imageVector, + contentDescription = null, + ) + } + } + } + } + } + } + preferredSize = JBUI.size(Dimension(800, 800)) + } + + override fun getComponent(): JComponent = composePanel + + override fun getPreferredFocusedComponent(): JComponent = composePanel + + override fun getName(): String = "Kotlin File Preview" + + override fun setState(state: FileEditorState) {} + + override fun isModified(): Boolean = false + + override fun isValid(): Boolean = true + + override fun addPropertyChangeListener(listener: java.beans.PropertyChangeListener) {} + + override fun removePropertyChangeListener(listener: java.beans.PropertyChangeListener) {} + + override fun getUserData(p0: Key): T? = null + + override fun putUserData(p0: Key, p1: T?) = Unit + + override fun dispose() {} +} + +private fun VirtualFile.toKtFile(project: Project): KtFile? { + val fileContent = contentsToByteArray().toString(Charsets.UTF_8) + + return PsiFileFactory.getInstance(project) + .createFileFromText(name, KotlinFileType.INSTANCE, fileContent) as? KtFile +} diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ui/viewmodel/ExistingPackViewModel.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ui/viewmodel/ExistingPackViewModel.kt index 49b5759f..55e8aa2f 100644 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ui/viewmodel/ExistingPackViewModel.kt +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ui/viewmodel/ExistingPackViewModel.kt @@ -2,7 +2,7 @@ package io.github.composegears.valkyrie.ui.screen.mode.iconpack.existingpack.ui. import com.composegears.tiamat.TiamatViewModel import com.intellij.openapi.project.Project -import io.github.composegears.valkyrie.extensions.castOrNull +import io.github.composegears.valkyrie.extensions.safeAs import io.github.composegears.valkyrie.generator.iconpack.IconPackGenerator import io.github.composegears.valkyrie.generator.iconpack.IconPackGeneratorConfig import io.github.composegears.valkyrie.psi.iconpack.IconPackInfo @@ -96,7 +96,7 @@ class ExistingPackViewModel( } private fun previewIconPackObject() = viewModelScope.launch { - val editState = currentState.castOrNull()?.packEditState ?: return@launch + val editState = currentState.safeAs()?.packEditState ?: return@launch val inputFieldState = editState.inputFieldState val iconPackCode = IconPackGenerator.create( @@ -111,7 +111,7 @@ class ExistingPackViewModel( } private fun saveIconPack() { - val editState = currentState.castOrNull() ?: return + val editState = currentState.safeAs() ?: return val inputFieldState = editState.packEditState.inputFieldState viewModelScope.launch { diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/ui/viewmodel/NewPackViewModel.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/ui/viewmodel/NewPackViewModel.kt index 8926d98d..c0f6003f 100644 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/ui/viewmodel/NewPackViewModel.kt +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/ui/viewmodel/NewPackViewModel.kt @@ -1,7 +1,7 @@ package io.github.composegears.valkyrie.ui.screen.mode.iconpack.newpack.ui.viewmodel import com.composegears.tiamat.TiamatViewModel -import io.github.composegears.valkyrie.extensions.castOrNull +import io.github.composegears.valkyrie.extensions.safeAs import io.github.composegears.valkyrie.generator.iconpack.IconPackGenerator import io.github.composegears.valkyrie.generator.iconpack.IconPackGeneratorConfig import io.github.composegears.valkyrie.parser.PackageExtractor @@ -100,7 +100,7 @@ class NewPackViewModel( } private fun saveDestination() { - val directoryState = currentState.castOrNull() ?: return + val directoryState = currentState.safeAs() ?: return inMemorySettings.updateIconPackDestination(directoryState.iconPackDestination) } @@ -112,7 +112,7 @@ class NewPackViewModel( } private fun previewIconPackObject() = viewModelScope.launch { - val editState = currentState.castOrNull()?.packEditState ?: return@launch + val editState = currentState.safeAs()?.packEditState ?: return@launch val inputFieldState = editState.inputFieldState val iconPackCode = IconPackGenerator.create( @@ -126,7 +126,7 @@ class NewPackViewModel( } private fun saveIconPack() { - val packEditState = currentState.castOrNull()?.packEditState ?: return + val packEditState = currentState.safeAs()?.packEditState ?: return viewModelScope.launch { IconPackWriter.savePack( diff --git a/idea-plugin/src/main/resources/META-INF/plugin.xml b/idea-plugin/src/main/resources/META-INF/plugin.xml index 8123789a..1d28804a 100644 --- a/idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/idea-plugin/src/main/resources/META-INF/plugin.xml @@ -38,6 +38,8 @@ Allows to create organized icon pack with an extension property of you pack obje icon="icons/ic_logo_tool_window.svg"/> + + @@ -45,3 +47,4 @@ Allows to create organized icon pack with an extension property of you pack obje + diff --git a/settings.gradle.kts b/settings.gradle.kts index 4fe71196..d40f7697 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,6 +51,8 @@ include("components:extensions") include("components:generator:common") include("components:generator:iconpack") include("components:generator:imagevector") +include("components:ir") include("components:parser") include("components:psi:iconpack") +include("components:psi:imagevector") include("playground:app")