mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 06:50:54 +07:00
[markdown][compose] IJPL-175966 Provide smoother animation
(cherry picked from commit 12880ed9794077b5f6ad185fe38c25113f1d53a2) (cherry picked from commit 14208dba7b101b9760f25156ce7253d9984453dd) IJ-MR-155570 GitOrigin-RevId: e1451e4e94dba8c97aaa46e50cca401919a2d357
This commit is contained in:
committed by
intellij-monorepo-bot
parent
0eab3bb84a
commit
0e36c06321
@@ -581,6 +581,7 @@ a:org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer
|
||||
- pa:afterProcessing():V
|
||||
- pa:beforeProcessing():V
|
||||
- f:process(kotlin.jvm.functions.Function0):java.lang.Object
|
||||
- a:scrollToLine(I,kotlin.coroutines.Continuation):java.lang.Object
|
||||
- a:scrollToLine(I,androidx.compose.animation.core.AnimationSpec,kotlin.coroutines.Continuation):java.lang.Object
|
||||
- bs:scrollToLine$default(org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer,I,androidx.compose.animation.core.AnimationSpec,kotlin.coroutines.Continuation,I,java.lang.Object):java.lang.Object
|
||||
f:org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer$Companion
|
||||
- f:create(androidx.compose.foundation.gestures.ScrollableState):org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Apache 2.0 license.
|
||||
package org.jetbrains.jewel.markdown.scrolling
|
||||
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.SpringSpec
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.gestures.ScrollableState
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
@@ -69,7 +71,7 @@ import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
|
||||
@ExperimentalJewelApi
|
||||
public abstract class ScrollingSynchronizer {
|
||||
/** Scroll the preview to the position that match the given [sourceLine] the best. */
|
||||
public abstract suspend fun scrollToLine(sourceLine: Int)
|
||||
public abstract suspend fun scrollToLine(sourceLine: Int, animationSpec: AnimationSpec<Float> = SpringSpec())
|
||||
|
||||
/**
|
||||
* Called when [MarkdownProcessor] processes the raw markdown text. The processing itself is passed as an [action].
|
||||
@@ -146,7 +148,7 @@ public abstract class ScrollingSynchronizer {
|
||||
// so this map always keeps relevant information.
|
||||
private val blocks2TextOffsets = mutableMapOf<MarkdownBlock, List<Int>>()
|
||||
|
||||
override suspend fun scrollToLine(sourceLine: Int) {
|
||||
override suspend fun scrollToLine(sourceLine: Int, animationSpec: AnimationSpec<Float>) {
|
||||
val block = findBestBlockForLine(sourceLine) ?: return
|
||||
val y = blocks2Top[block] ?: return
|
||||
if (y < 0) return
|
||||
@@ -156,7 +158,7 @@ public abstract class ScrollingSynchronizer {
|
||||
// in this case scroll to the first line of the first block positioned after the line
|
||||
val lineIndexInBlock = maxOf(0, sourceLine - lineRange.start)
|
||||
val lineOffset = textOffsets?.get(lineIndexInBlock) ?: 0
|
||||
scrollState.animateScrollTo(y + lineOffset)
|
||||
scrollState.animateScrollTo(y + lineOffset, animationSpec)
|
||||
}
|
||||
|
||||
private fun findBestBlockForLine(line: Int): MarkdownBlock? {
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import java.util.Arrays
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -59,7 +60,6 @@ import org.jetbrains.jewel.ui.component.styling.TrackClickBehavior
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.util.Arrays
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class ScrollingSynchronizerTest {
|
||||
|
||||
@@ -1,38 +1,50 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.markdown.compose.preview
|
||||
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.intellij.ide.BrowserUtil
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.openapi.util.UserDataHolderBase
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.platform.compose.JBComposePanel
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import org.intellij.plugins.markdown.ui.preview.MarkdownHtmlPanel
|
||||
import org.intellij.plugins.markdown.ui.preview.MarkdownHtmlPanelEx
|
||||
import org.intellij.plugins.markdown.ui.preview.MarkdownUpdateHandler
|
||||
import org.intellij.plugins.markdown.ui.preview.MarkdownUpdateHandler.PreviewRequest
|
||||
import org.intellij.plugins.markdown.ui.preview.PreviewStyleScheme
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.jewel.bridge.code.highlighting.CodeHighlighterFactory
|
||||
import org.jetbrains.jewel.bridge.toComposeColor
|
||||
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
|
||||
import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter
|
||||
import org.jetbrains.jewel.intui.markdown.bridge.ProvideMarkdownStyling
|
||||
import org.jetbrains.jewel.markdown.Markdown
|
||||
import org.jetbrains.jewel.ui.component.VerticalScrollbar
|
||||
import org.jetbrains.jewel.markdown.MarkdownMode
|
||||
import org.jetbrains.jewel.markdown.rendering.DefaultInlineMarkdownRenderer
|
||||
import org.jetbrains.jewel.markdown.scrolling.ScrollSyncMarkdownBlockRenderer
|
||||
import org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer
|
||||
import javax.swing.JComponent
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@ExperimentalJewelApi
|
||||
class MarkdownComposePanel(
|
||||
internal class MarkdownComposePanel(
|
||||
private val project: Project?,
|
||||
private val virtualFile: VirtualFile?,
|
||||
private val updateHandler: MarkdownUpdateHandler = MarkdownUpdateHandler.Debounced()
|
||||
@@ -40,6 +52,8 @@ class MarkdownComposePanel(
|
||||
|
||||
constructor() : this(null, null)
|
||||
|
||||
private val scrollToLineFlow = MutableSharedFlow<Int>(replay = 1)
|
||||
|
||||
private val panelComponent by lazy {
|
||||
JBComposePanel {
|
||||
// TODO temporary styling, we will likely need our own in the future for JCEF-like rendering
|
||||
@@ -52,16 +66,26 @@ class MarkdownComposePanel(
|
||||
private fun MarkdownPanel() {
|
||||
val scheme = PreviewStyleScheme.fromCurrentTheme()
|
||||
val fontSize = scheme.fontSize.sp / scheme.scale
|
||||
val scrollState = rememberScrollState(0)
|
||||
val scrollingSynchronizer = remember(scrollState) { ScrollingSynchronizer.create(scrollState) }
|
||||
val markdownStyling = JcefLikeMarkdownStyling(scheme, fontSize)
|
||||
val blockRenderer = ScrollSyncMarkdownBlockRenderer(markdownStyling, emptyList(), DefaultInlineMarkdownRenderer(emptyList()))
|
||||
ProvideMarkdownStyling(
|
||||
markdownStyling = JcefLikeMarkdownStyling(scheme, fontSize),
|
||||
markdownMode = MarkdownMode.EditorPreview(scrollingSynchronizer),
|
||||
markdownStyling = markdownStyling,
|
||||
codeHighlighter = remember(project) {
|
||||
project?.let {
|
||||
CodeHighlighterFactory.getInstance(project).createHighlighter()
|
||||
} ?: NoOpCodeHighlighter
|
||||
},
|
||||
markdownBlockRenderer = blockRenderer
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(scheme.backgroundColor.toComposeColor())
|
||||
.padding(horizontal = fontSize.value.dp * 2)
|
||||
) {
|
||||
val scrollState = rememberScrollState(0)
|
||||
MarkdownPreviewPanel(scrollState)
|
||||
MarkdownPreviewPanel(scrollState, scrollingSynchronizer, blockRenderer)
|
||||
VerticalScrollbar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd),
|
||||
@@ -71,11 +95,33 @@ class MarkdownComposePanel(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Suppress("FunctionName")
|
||||
@Composable
|
||||
private fun MarkdownPreviewPanel(scrollState: ScrollState) {
|
||||
private fun MarkdownPreviewPanel(scrollState: ScrollState,
|
||||
scrollingSynchronizer: ScrollingSynchronizer?,
|
||||
blockRenderer: ScrollSyncMarkdownBlockRenderer,
|
||||
animationSpec: AnimationSpec<Float> = TweenSpec(easing = LinearEasing)
|
||||
) {
|
||||
val request by updateHandler.requests.collectAsState(null)
|
||||
(request as? PreviewRequest.Update)?.let {
|
||||
if (scrollingSynchronizer != null) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch {
|
||||
scrollToLineFlow.debounce(16.milliseconds).collect { scrollToLine ->
|
||||
scrollingSynchronizer.scrollToLine(scrollToLine, animationSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(it.initialScrollOffset) {
|
||||
coroutineScope.launch {
|
||||
if (it.initialScrollOffset != 0) {
|
||||
scrollToLineFlow.emit(it.initialScrollOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Markdown(
|
||||
it.content,
|
||||
modifier = Modifier
|
||||
@@ -84,12 +130,16 @@ class MarkdownComposePanel(
|
||||
enabled = true,
|
||||
selectable = true,
|
||||
onUrlClick = { url -> BrowserUtil.open(url) },
|
||||
blockRenderer = blockRenderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setHtml(html: String, initialScrollOffset: Int, document: VirtualFile?) {
|
||||
updateHandler.setContent(html, initialScrollOffset, document)
|
||||
}
|
||||
|
||||
override fun setHtml(html: String, initialScrollOffset: Int, initialScrollLineNumber: Int, document: VirtualFile?) {
|
||||
updateHandler.setContent(html, initialScrollLineNumber, document)
|
||||
}
|
||||
|
||||
override fun reloadWithOffset(offset: Int) {
|
||||
@@ -115,7 +165,8 @@ class MarkdownComposePanel(
|
||||
override fun removeScrollListener(listener: MarkdownHtmlPanel.ScrollListener) {
|
||||
}
|
||||
|
||||
override fun scrollToMarkdownSrcOffset(offset: Int, smooth: Boolean) {
|
||||
override suspend fun scrollTo(editor: Editor, line: Int) {
|
||||
scrollToLineFlow.emit(line)
|
||||
}
|
||||
|
||||
override fun scrollBy(horizontalUnits: Int, verticalUnits: Int) {
|
||||
|
||||
Reference in New Issue
Block a user