Skip to content

Commit

Permalink
Add visual transformation support in sticky header
Browse files Browse the repository at this point in the history
  • Loading branch information
zhelenskiy committed Apr 4, 2024
1 parent c5d2d7b commit 783c41a
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 32 deletions.
18 changes: 9 additions & 9 deletions composeApp/src/commonMain/kotlin/CorrectBracketSequence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,14 @@ fun CorrectBracketSequence() {
val coroutineScope = rememberCoroutineScope()
val externalScrollToFlow = remember { MutableSharedFlow<SourceCodePosition>() }
val showLineNumbers by remember { mutableStateOf(true) }
val pinLines by remember { mutableStateOf(true) }
val stickyHeader by remember { mutableStateOf(true) }
val showIndentation by remember { mutableStateOf(true) }
val textSize = measureText(textStyle)
val density = LocalDensity.current
val pinLinesChooser: (Bracket) -> IntRange? = { bracket ->
val stickyHeaderLinesChooser: (Bracket) -> IntRange? = { bracket ->
if (bracket.bracket in "{}") codeTextFieldState.tokenLines[bracket] else null
}
var maximumPinnedLinesHeight: Dp by remember { mutableStateOf(0.dp) }
var maximumStickyHeaderHeight: Dp by remember { mutableStateOf(0.dp) }

val lineNumbersColor = Color.DarkGray
BasicSourceCodeTextField(
Expand All @@ -140,17 +140,17 @@ fun CorrectBracketSequence() {
manualScrollToPosition = externalScrollToFlow,
additionalOuterComposable = { _, inner ->
inner()
AnimatedVisibility(pinLines) {
PinnedLines(
AnimatedVisibility(stickyHeader) {
StickyHeader(
state = codeTextFieldState,
textStyle = textStyle,
lineNumbersColor = lineNumbersColor,
backgroundColor = Color.White,
scrollState = verticalState,
showLineNumbers = showLineNumbers,
matchedBrackets = matchedBrackets,
pinLinesChooser = pinLinesChooser,
maximumPinnedLinesHeight = (maxHeight / 3).also { maximumPinnedLinesHeight = it },
stickyHeaderLinesChooser = stickyHeaderLinesChooser,
maximumStickyHeaderHeight = (maxHeight / 3).also { maximumStickyHeaderHeight = it },
onClick = { coroutineScope.launch { externalScrollToFlow.emit(SourceCodePosition(it, 0)) } },
divider = { HorizontalDivider(thickness = 1.dp) },
additionalInnerComposable = { linesToWrite, _ ->
Expand Down Expand Up @@ -180,8 +180,8 @@ fun CorrectBracketSequence() {
state = codeTextFieldState,
matchedBrackets = matchedBrackets,
dividerThickness = 0.dp, // do not include divider thickness in the calculation
maximumPinnedLinesHeight = maximumPinnedLinesHeight,
pinLinesChooser = pinLinesChooser,
maximumStickyHeaderHeight = maximumStickyHeaderHeight,
stickyHeaderLinesChooser = stickyHeaderLinesChooser,
)
)
},
Expand Down
47 changes: 25 additions & 22 deletions editor/src/commonMain/kotlin/editor/basic/Decorators.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
Expand Down Expand Up @@ -74,11 +75,11 @@ public fun BoxScope.IndentationLines(
}

@PublishedApi
internal inline fun <reified Bracket : ScopeChangingToken, T : Token> getPinnedLines(
internal inline fun <reified Bracket : ScopeChangingToken, T : Token> getStickyHeaderLines(
line: Int,
state: BasicSourceCodeTextFieldState<T>,
matchedBrackets: Map<Bracket, Bracket>,
crossinline pinLinesChooser: (Bracket) -> IntRange? = { bracket -> state.tokenLines[bracket as T] },
crossinline stickyHeaderLinesChooser: (Bracket) -> IntRange? = { bracket -> state.tokenLines[bracket as T] },
): Set<Int> {
val lineUsages = IntArray(state.offsets.size)
var topLine = line
Expand Down Expand Up @@ -125,7 +126,7 @@ internal inline fun <reified Bracket : ScopeChangingToken, T : Token> getPinnedL
}
}
return openedBracketLines.flatMapTo(mutableSetOf()) { bracket ->
pinLinesChooser(bracket) ?: IntRange.EMPTY
stickyHeaderLinesChooser(bracket) ?: IntRange.EMPTY
}.sorted().toSet()
}

Expand All @@ -136,19 +137,19 @@ public inline fun <reified Bracket : ScopeChangingToken, T : Token> getOffsetFor
state: BasicSourceCodeTextFieldState<T>,
matchedBrackets: Map<Bracket, Bracket>,
dividerThickness: Dp,
maximumPinnedLinesHeight: Dp,
crossinline pinLinesChooser: (Bracket) -> IntRange? = { bracket -> state.tokenLines[bracket as T] }
maximumStickyHeaderHeight: Dp,
crossinline stickyHeaderLinesChooser: (Bracket) -> IntRange? = { bracket -> state.tokenLines[bracket as T] }
): Int {
val resultLine = (line downTo 0).firstOrNull { attemptLine ->
val height = getPinnedLinesHeight(
val height = getStickyHeaderHeight(
attemptLine,
textSize,
density,
state,
matchedBrackets,
dividerThickness,
maximumPinnedLinesHeight,
pinLinesChooser
maximumStickyHeaderHeight,
stickyHeaderLinesChooser
)
(line - attemptLine) * textSize.height >= height
} ?: 0
Expand All @@ -157,25 +158,25 @@ public inline fun <reified Bracket : ScopeChangingToken, T : Token> getOffsetFor


@PublishedApi
internal inline fun <reified Bracket : ScopeChangingToken, T : Token> getPinnedLinesHeight(
internal inline fun <reified Bracket : ScopeChangingToken, T : Token> getStickyHeaderHeight(
line: Int,
textSize: Size,
density: Density,
state: BasicSourceCodeTextFieldState<T>,
matchedBrackets: Map<Bracket, Bracket>,
dividerThickness: Dp,
maximumPinnedLinesHeight: Dp,
crossinline pinLinesChooser: (Bracket) -> IntRange? = { bracket -> state.tokenLines[bracket as T] }
maximumStickyHeaderHeight: Dp,
crossinline stickyHeaderLinesChooser: (Bracket) -> IntRange? = { bracket -> state.tokenLines[bracket as T] }
): Int {
val pinnedLines = getPinnedLines<Bracket, T>(line, state, matchedBrackets, pinLinesChooser)
if (pinnedLines.isEmpty()) return 0
val stickyHeaderLines = getStickyHeaderLines<Bracket, T>(line, state, matchedBrackets, stickyHeaderLinesChooser)
if (stickyHeaderLines.isEmpty()) return 0
return with(density) {
minOf(pinnedLines.size * textSize.height + dividerThickness.toPx(), maximumPinnedLinesHeight.toPx())
minOf(stickyHeaderLines.size * textSize.height + dividerThickness.toPx(), maximumStickyHeaderHeight.toPx())
}.roundToInt()
}

@Composable
public inline fun <reified Bracket : ScopeChangingToken, T : Token> BoxWithConstraintsScope.PinnedLines(
public inline fun <reified Bracket : ScopeChangingToken, T : Token> BoxWithConstraintsScope.StickyHeader(
state: BasicSourceCodeTextFieldState<T>,
textStyle: TextStyle,
lineNumbersColor: Color,
Expand All @@ -184,10 +185,11 @@ public inline fun <reified Bracket : ScopeChangingToken, T : Token> BoxWithConst
showLineNumbers: Boolean,
matchedBrackets: Map<Bracket, Bracket>,
divider: @Composable () -> Unit,
maximumPinnedLinesHeight: Dp = maxHeight / 3,
maximumStickyHeaderHeight: Dp = maxHeight / 3,
lineNumberModifier: Modifier = defaultLineNumberModifier,
lineStringModifier: Modifier = Modifier,
crossinline pinLinesChooser: (Bracket) -> IntRange? = { bracket -> state.tokenLines[bracket as T] },
visualTransformation: VisualTransformation = VisualTransformation.None,
crossinline stickyHeaderLinesChooser: (Bracket) -> IntRange? = { bracket -> state.tokenLines[bracket as T] },
crossinline onClick: (lineNumber: Int) -> Unit = {},
crossinline onHoveredSourceCodePositionChange: (position: SourceCodePosition) -> Unit = {},
crossinline additionalInnerComposable: @Composable BoxWithConstraintsScope.(linesToWrite: Map<Int, AnnotatedString>, inner: @Composable () -> Unit) -> Unit = { _, _ -> },
Expand All @@ -196,24 +198,25 @@ public inline fun <reified Bracket : ScopeChangingToken, T : Token> BoxWithConst
val textHeightDp = with(LocalDensity.current) { measuredText.height.toDp() }
if (scrollState.value == 0) return
val topVisibleRow = (scrollState.value / measuredText.height).toInt()
val requestedLinesSet = getPinnedLines(topVisibleRow, state, matchedBrackets, pinLinesChooser)
val requestedLinesSet = getStickyHeaderLines(topVisibleRow, state, matchedBrackets, stickyHeaderLinesChooser)
if (requestedLinesSet.isEmpty()) return
Column {
Column(Modifier.heightIn(max = maximumPinnedLinesHeight)) {
Column(Modifier.heightIn(max = maximumStickyHeaderHeight)) {
Row(
modifier = Modifier
.width(this@PinnedLines.maxWidth)
.width(this@StickyHeader.maxWidth)
.verticalScroll(rememberScrollState())
.background(backgroundColor)
) {
val lineCount: Int = state.offsets.size
val linesToWrite = requestedLinesSet.associateWith { lineNumber ->
val lineOffsets =
state.offsets[lineNumber].takeIf { it.isNotEmpty() } ?: return@associateWith AnnotatedString("")
val annotatedString = visualTransformation.filter(state.annotatedString).text
val lastOffset =
if (lineOffsets.last() == state.text.lastIndex) state.text.length else lineOffsets.last()
if (lineOffsets.last() == annotatedString.lastIndex) annotatedString.length else lineOffsets.last()
buildAnnotatedString {
append(state.annotatedString, lineOffsets.first(), lastOffset)
append(annotatedString, lineOffsets.first(), lastOffset)
}
}
AnimatedVisibility(showLineNumbers) {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ development=true

#Publication
group=com.zhelenskiy
version=0.0.5
version=0.0.7

0 comments on commit 783c41a

Please sign in to comment.