Skip to content

Commit

Permalink
Introduce ImageVectorPreview actions
Browse files Browse the repository at this point in the history
  • Loading branch information
egorikftp committed Aug 30, 2024
1 parent b47e1a9 commit 8bfe0f2
Show file tree
Hide file tree
Showing 24 changed files with 1,310 additions and 94 deletions.
2 changes: 1 addition & 1 deletion components/parser/ktfile/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ dependencies {
implementation(projects.components.ir)
implementation(projects.components.psi.imagevector)

implementation(compose.material3)
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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 androidx.compose.ui.unit.dp
import io.github.composegears.valkyrie.ir.IrFill
import io.github.composegears.valkyrie.ir.IrImageVector
Expand Down Expand Up @@ -39,11 +40,14 @@ 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 {
fun IrImageVector.toComposeImageVector(
width: Dp = Dp.Unspecified,
height: Dp = Dp.Unspecified,
): ImageVector {
return ImageVector.Builder(
name = name,
defaultWidth = defaultWidth.dp,
defaultHeight = defaultHeight.dp,
defaultWidth = if (width == Dp.Unspecified) defaultWidth.dp else width,
defaultHeight = if (height == Dp.Unspecified) defaultHeight.dp else height,
viewportWidth = viewportWidth,
viewportHeight = viewportHeight,
autoMirror = autoMirror,
Expand Down
1 change: 1 addition & 0 deletions idea-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
implementation(projects.components.parser.ktfile)
implementation(projects.components.parser.svgxml)
implementation(projects.components.psi.iconpack)
implementation(projects.components.psi.imagevector)

compileOnly(compose.desktop.currentOs)
implementation(compose.desktop.common)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
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
Expand All @@ -19,16 +9,11 @@ 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.extensions.safeAs
import io.github.composegears.valkyrie.parser.ktfile.KtFileToImageVectorParser
import io.github.composegears.valkyrie.ui.foundation.PixelGrid
import io.github.composegears.valkyrie.editor.ui.ImageVectorPreviewPanel
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,
Expand Down Expand Up @@ -63,34 +48,7 @@ private class ImageVectorPreviewEditor(
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 {
KtFileToImageVectorParser.parse(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,
)
}
}
}
}
ImageVectorPreviewPanel(file)
}
}
preferredSize = JBUI.size(Dimension(800, 800))
Expand Down Expand Up @@ -118,11 +76,3 @@ private class ImageVectorPreviewEditor(

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)
.safeAs<KtFile>()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.github.composegears.valkyrie.editor

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFileFactory
import io.github.composegears.valkyrie.extensions.safeAs
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.psi.KtFile

internal fun VirtualFile.toKtFile(project: Project): KtFile? {
val fileContent = contentsToByteArray().toString(Charsets.UTF_8)

return PsiFileFactory.getInstance(project)
.createFileFromText(name, KotlinFileType.INSTANCE, fileContent)
.safeAs<KtFile>()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
@file:Suppress("NAME_SHADOWING")

package io.github.composegears.valkyrie.editor.ui

import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.min
import androidx.compose.ui.unit.times
import com.intellij.openapi.application.readAction
import com.intellij.openapi.vfs.VirtualFile
import io.github.composegears.valkyrie.editor.toKtFile
import io.github.composegears.valkyrie.editor.ui.error.PreviewParsingError
import io.github.composegears.valkyrie.editor.ui.previewer.ImageVectorPreview
import io.github.composegears.valkyrie.editor.ui.previewer.rememberZoomState
import io.github.composegears.valkyrie.ir.IrImageVector
import io.github.composegears.valkyrie.parser.ktfile.util.toComposeImageVector
import io.github.composegears.valkyrie.psi.imagevector.ImageVectorPsiParser
import io.github.composegears.valkyrie.ui.foundation.rememberMutableState
import io.github.composegears.valkyrie.ui.foundation.theme.LocalProject
import kotlin.math.min
import kotlinx.coroutines.launch
import org.jetbrains.kotlin.psi.KtFile

sealed interface PanelState {
data object Initial : PanelState
data object Error : PanelState
data class Success(val imageVector: ImageVector) : PanelState
}

@Composable
fun ImageVectorPreviewPanel(
file: VirtualFile,
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
val project = LocalProject.current

val scope = rememberCoroutineScope()
val zoomState = rememberZoomState()

var panelState by rememberMutableState<PanelState> { PanelState.Initial }

var initialViewportWidth by rememberMutableState { Dp.Unspecified }
var initialViewportHeight by rememberMutableState { Dp.Unspecified }

val ktFile by produceState<KtFile?>(null) {
value = readAction {
file.toKtFile(project)
}
}
var irImageVector by rememberMutableState<IrImageVector?> { null }

LaunchedEffect(ktFile) {
val ktFile = ktFile ?: return@LaunchedEffect

readAction {
irImageVector = ImageVectorPsiParser.parseToIrImageVector(ktFile)?.also {
initialViewportWidth = it.defaultWidth.dp
initialViewportHeight = it.defaultHeight.dp

val maxPreviewSize = zoomState.maxPreviewSize
val initialScale = maxPreviewSize / (3 * min(initialViewportWidth, initialViewportHeight))

launch {
zoomState.setScale(initialScale)
}
}
}

if (irImageVector == null) {
panelState = PanelState.Error
}
}

LaunchedEffect(irImageVector, zoomState.scale) {
val irImageVector = irImageVector ?: return@LaunchedEffect

panelState = PanelState.Success(
imageVector = irImageVector.toComposeImageVector(
width = (initialViewportWidth.value * zoomState.scale).dp,
height = (initialViewportHeight.value * zoomState.scale).dp,
),
)
}

BoxWithConstraints(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
zoomState.maxPreviewSize = min(maxWidth, maxHeight)

when (val state = panelState) {
is PanelState.Error -> PreviewParsingError()
is PanelState.Success -> ImageVectorPreview(
imageVector = state.imageVector,
defaultWidth = initialViewportWidth.value,
defaultHeight = initialViewportHeight.value,
zoomIn = {
if (zoomState.maxPreviewSize >= min(
state.imageVector.defaultWidth,
state.imageVector.defaultHeight,
)
) {
scope.launch {
zoomState.zoomIn()
}
}
},
zoomOut = {
scope.launch {
zoomState.zoomOut()
}
},
reset = {
scope.launch {
zoomState.reset()
}
},
fitToWindow = {
scope.launch {
with(density) {
val scaleFactor = min(
zoomState.maxPreviewSize.toPx() / initialViewportWidth.toPx(),
zoomState.maxPreviewSize.toPx() / initialViewportHeight.toPx(),
)
zoomState.animateToScale(scaleFactor)
}
}
},
)
else -> {}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.github.composegears.valkyrie.editor.ui.error

import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.UrlAnnotation
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withAnnotation
import androidx.compose.ui.unit.dp
import io.github.composegears.valkyrie.ui.foundation.ClickableText
import io.github.composegears.valkyrie.ui.foundation.WeightSpacer
import io.github.composegears.valkyrie.ui.foundation.icons.Error
import io.github.composegears.valkyrie.ui.foundation.icons.ValkyrieIcons
import io.github.composegears.valkyrie.ui.foundation.platform.rememberBrowserUtil
import io.github.composegears.valkyrie.ui.foundation.theme.PreviewTheme

@Composable
internal fun PreviewParsingError() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
WeightSpacer(weight = 0.3f)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
) {
Image(
imageVector = ValkyrieIcons.Error,
contentDescription = null,
)
ErrorMessage()
}
WeightSpacer(weight = 0.7f)
}
}

@OptIn(ExperimentalTextApi::class)
@Composable
private fun ErrorMessage() {
val browserUtil = rememberBrowserUtil()

val underlineColor = MaterialTheme.colorScheme.primary
val annotatedString = buildAnnotatedString {
append("Failed to preview ImageVector, please ")

withAnnotation(UrlAnnotation("https://github.com/ComposeGears/Valkyrie/issues")) {
append(
AnnotatedString(
"submit issue",
spanStyle = SpanStyle(
color = underlineColor,
textDecoration = TextDecoration.Underline,
),
),
)
}
append(" with reproducer.")
}
ClickableText(
annotatedString = annotatedString,
onClick = browserUtil::open,
)
}

@Preview
@Composable
private fun PreviewParsingErrorPreview() = PreviewTheme {
PreviewParsingError()
}
Loading

0 comments on commit 8bfe0f2

Please sign in to comment.