diff --git a/components/ir/src/main/kotlin/io/github/composegears/valkyrie/ir/Stub.kt b/components/ir/src/main/kotlin/io/github/composegears/valkyrie/ir/Stub.kt new file mode 100644 index 00000000..91b95925 --- /dev/null +++ b/components/ir/src/main/kotlin/io/github/composegears/valkyrie/ir/Stub.kt @@ -0,0 +1,10 @@ +package io.github.composegears.valkyrie.ir + +val STUB = IrImageVector( + name = "", + defaultWidth = 24.0f, + defaultHeight = 24.0f, + viewportWidth = 18.0f, + viewportHeight = 18.0f, + nodes = emptyList(), +) 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 index e63e06e2..44d7fd07 100644 --- 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 @@ -1,5 +1,7 @@ package io.github.composegears.valkyrie.editor +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier import androidx.compose.ui.awt.ComposePanel import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditorState @@ -10,7 +12,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.ui.JBUI -import io.github.composegears.valkyrie.editor.ui.ImageVectorPreviewPanel +import io.github.composegears.valkyrie.editor.ui.ImageVectorFilePreviewPanel import io.github.composegears.valkyrie.ui.foundation.theme.ValkyrieTheme import java.awt.Dimension import javax.swing.JComponent @@ -48,7 +50,10 @@ private class ImageVectorPreviewEditor( private val composePanel = ComposePanel().apply { setContent { ValkyrieTheme(project, this) { - ImageVectorPreviewPanel(file) + ImageVectorFilePreviewPanel( + file = file, + modifier = Modifier.fillMaxSize(), + ) } } preferredSize = JBUI.size(Dimension(800, 800)) diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/ImageVectorFilePreviewPanel.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/ImageVectorFilePreviewPanel.kt new file mode 100644 index 00000000..1b925684 --- /dev/null +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/ImageVectorFilePreviewPanel.kt @@ -0,0 +1,54 @@ +@file:Suppress("NAME_SHADOWING") + +package io.github.composegears.valkyrie.editor.ui + +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.setValue +import androidx.compose.ui.Modifier +import com.intellij.openapi.application.readAction +import com.intellij.openapi.vfs.VirtualFile +import io.github.composegears.valkyrie.editor.toKtFile +import io.github.composegears.valkyrie.ir.IrImageVector +import io.github.composegears.valkyrie.psi.imagevector.ImageVectorPsiParser +import io.github.composegears.valkyrie.ui.foundation.components.previewer.ImageVectorPreviewPanel +import io.github.composegears.valkyrie.ui.foundation.rememberMutableState +import io.github.composegears.valkyrie.ui.foundation.theme.LocalProject +import org.jetbrains.kotlin.psi.KtFile + +@Composable +fun ImageVectorFilePreviewPanel( + file: VirtualFile, + modifier: Modifier = Modifier, +) { + val project = LocalProject.current + + var irImageVector by rememberMutableState { null } + var isPreparing by rememberMutableState { true } + + val ktFile by produceState(null) { + value = readAction { + file.toKtFile(project) + } + } + + LaunchedEffect(ktFile) { + val ktFile = ktFile ?: return@LaunchedEffect + + readAction { + isPreparing = true + irImageVector = ImageVectorPsiParser.parseToIrImageVector(ktFile) + isPreparing = false + } + } + + if (!isPreparing) { + ImageVectorPreviewPanel( + modifier = modifier.fillMaxSize(), + irImageVector = irImageVector, + ) + } +} diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/ImageVectorPreviewPanel.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/ImageVectorPreviewPanel.kt deleted file mode 100644 index a1fc01fc..00000000 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/ImageVectorPreviewPanel.kt +++ /dev/null @@ -1,146 +0,0 @@ -@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.Initial } - - var initialViewportWidth by rememberMutableState { Dp.Unspecified } - var initialViewportHeight by rememberMutableState { Dp.Unspecified } - - val ktFile by produceState(null) { - value = readAction { - file.toKtFile(project) - } - } - var irImageVector by rememberMutableState { 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( - defaultWidth = (initialViewportWidth.value * zoomState.scale).dp, - defaultHeight = (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 -> {} - } - } -} diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/previewer/ImageVectorPreview.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/previewer/ImageVectorPreview.kt deleted file mode 100644 index 8519e050..00000000 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/previewer/ImageVectorPreview.kt +++ /dev/null @@ -1,111 +0,0 @@ -package io.github.composegears.valkyrie.editor.ui.previewer - -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -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 -import io.github.composegears.valkyrie.ui.foundation.previewbg.BgType.PixelGrid -import io.github.composegears.valkyrie.ui.foundation.previewbg.PreviewBackground -import io.github.composegears.valkyrie.ui.foundation.rememberMutableState -import io.github.composegears.valkyrie.ui.foundation.theme.PreviewTheme - -@Composable -fun ImageVectorPreview( - imageVector: ImageVector, - defaultWidth: Float, - defaultHeight: Float, - zoomIn: () -> Unit, - zoomOut: () -> Unit, - reset: () -> Unit, - modifier: Modifier = Modifier, - fitToWindow: () -> Unit, -) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - var bgType by rememberMutableState { PixelGrid } - - BoxWithConstraints( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Box { - PreviewBackground( - bgType = bgType, - modifier = Modifier.matchParentSize(), - ) - Image( - imageVector = imageVector, - contentDescription = null, - ) - } - } - TopActions( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface) - .align(Alignment.TopStart) - .padding(horizontal = 12.dp, vertical = 8.dp), - defaultWidth = defaultWidth, - defaultHeight = defaultHeight, - onBgTypeChange = { - bgType = it - }, - zoomIn = zoomIn, - zoomOut = zoomOut, - onActualSize = reset, - fitToWindow = fitToWindow, - ) - } -} - -@Preview -@Composable -private fun ImageVectorPreviewPreview() = PreviewTheme { - ImageVectorPreview( - imageVector = ImageVector.Builder( - name = "Outlined.Add", - 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(), - defaultWidth = 24f, - defaultHeight = 24f, - zoomIn = {}, - zoomOut = {}, - reset = {}, - fitToWindow = {}, - ) -} diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/components/previewer/ImageVectorPreviewPanel.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/components/previewer/ImageVectorPreviewPanel.kt new file mode 100644 index 00000000..8854d9db --- /dev/null +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/components/previewer/ImageVectorPreviewPanel.kt @@ -0,0 +1,189 @@ +package io.github.composegears.valkyrie.ui.foundation.components.previewer + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +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 io.github.composegears.valkyrie.editor.ui.error.PreviewParsingError +import io.github.composegears.valkyrie.ir.IrImageVector +import io.github.composegears.valkyrie.ir.STUB +import io.github.composegears.valkyrie.parser.ktfile.util.toComposeImageVector +import io.github.composegears.valkyrie.ui.foundation.VerticalSpacer +import io.github.composegears.valkyrie.ui.foundation.previewbg.BgType.PixelGrid +import io.github.composegears.valkyrie.ui.foundation.previewbg.PreviewBackground +import io.github.composegears.valkyrie.ui.foundation.rememberMutableState +import io.github.composegears.valkyrie.ui.foundation.theme.PreviewTheme +import kotlin.math.min +import kotlinx.coroutines.launch + +sealed interface PanelState { + data object Initial : PanelState + data object Error : PanelState + data class Success(val imageVector: ImageVector) : PanelState +} + +@Composable +fun ImageVectorPreviewPanel( + irImageVector: IrImageVector?, + modifier: Modifier = Modifier, +) { + val density = LocalDensity.current + val scope = rememberCoroutineScope() + val zoomState = rememberZoomState() + + val initialWidth by rememberMutableState(irImageVector) { irImageVector?.defaultWidth?.dp ?: Dp.Unspecified } + val initialHeight by rememberMutableState(irImageVector) { irImageVector?.defaultHeight?.dp ?: Dp.Unspecified } + + var panelState by rememberMutableState { PanelState.Initial } + + LaunchedEffect(irImageVector, zoomState.maxPreviewSize) { + val maxPreviewSize = zoomState.maxPreviewSize + if (irImageVector == null || maxPreviewSize == Dp.Unspecified) return@LaunchedEffect + + val initialScale = maxPreviewSize / (3 * min(irImageVector.defaultWidth.dp, irImageVector.defaultHeight.dp)) + + zoomState.setScale(initialScale) + } + + LaunchedEffect(irImageVector, zoomState.scale) { + panelState = if (irImageVector == null || initialWidth == Dp.Unspecified || initialHeight == Dp.Unspecified) { + PanelState.Error + } else { + PanelState.Success( + imageVector = irImageVector.toComposeImageVector( + defaultWidth = (initialWidth.value * zoomState.scale).dp, + defaultHeight = (initialHeight.value * zoomState.scale).dp, + ), + ) + } + } + + BoxWithConstraints( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + zoomState.maxPreviewSize = min(maxWidth, maxHeight) + + when (val state = panelState) { + is PanelState.Error -> PreviewParsingError() + is PanelState.Success -> ImageVectorPreviewUi( + imageVector = state.imageVector, + defaultWidth = initialWidth.value, + defaultHeight = initialHeight.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() / initialWidth.toPx(), + zoomState.maxPreviewSize.toPx() / initialWidth.toPx(), + ) + zoomState.animateToScale(scaleFactor) + } + } + }, + ) + else -> {} + } + } +} + +@Composable +private fun ImageVectorPreviewUi( + imageVector: ImageVector, + defaultWidth: Float, + defaultHeight: Float, + zoomIn: () -> Unit, + zoomOut: () -> Unit, + reset: () -> Unit, + modifier: Modifier = Modifier, + fitToWindow: () -> Unit, +) { + Column( + modifier = modifier.padding(vertical = 8.dp), + verticalArrangement = Arrangement.Center, + ) { + var bgType by rememberMutableState { PixelGrid } + + TopActions( + modifier = Modifier + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 12.dp), + defaultWidth = defaultWidth, + defaultHeight = defaultHeight, + onBgTypeChange = { + bgType = it + }, + zoomIn = zoomIn, + zoomOut = zoomOut, + onActualSize = reset, + fitToWindow = fitToWindow, + ) + VerticalSpacer(8.dp) + Box( + modifier = Modifier + .fillMaxSize() + .align(Alignment.CenterHorizontally), + contentAlignment = Alignment.Center, + ) { + Box { + PreviewBackground( + bgType = bgType, + modifier = Modifier.matchParentSize(), + ) + Image( + imageVector = imageVector, + contentDescription = null, + ) + } + } + } +} + +@Preview +@Composable +private fun ImageVectorPreviewPanelPreview() = PreviewTheme { + ImageVectorPreviewPanel( + modifier = Modifier.fillMaxSize(), + irImageVector = STUB, + ) +} diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/previewer/TopActions.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/components/previewer/TopActions.kt similarity index 98% rename from idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/previewer/TopActions.kt rename to idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/components/previewer/TopActions.kt index 683f9034..d963f4d7 100644 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/previewer/TopActions.kt +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/components/previewer/TopActions.kt @@ -1,4 +1,4 @@ -package io.github.composegears.valkyrie.editor.ui.previewer +package io.github.composegears.valkyrie.ui.foundation.components.previewer import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Arrangement diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/previewer/ZoomState.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/components/previewer/ZoomState.kt similarity index 92% rename from idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/previewer/ZoomState.kt rename to idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/components/previewer/ZoomState.kt index 22e587a1..6e7cb913 100644 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/editor/ui/previewer/ZoomState.kt +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/components/previewer/ZoomState.kt @@ -1,4 +1,4 @@ -package io.github.composegears.valkyrie.editor.ui.previewer +package io.github.composegears.valkyrie.ui.foundation.components.previewer import androidx.compose.animation.core.Animatable import androidx.compose.runtime.Composable