From fc3554253a48a9a3b81f54dcfe4cabfa05d566e4 Mon Sep 17 00:00:00 2001 From: Sunny Chung Date: Sun, 10 Dec 2023 00:46:33 +0800 Subject: [PATCH 1/8] [WIP] add line number display to code editor --- .../hellohttp/extension/ListExtension.kt | 12 ++ .../hellohttp/ux/CodeEditorView.kt | 151 +++++++++++++----- 2 files changed, 120 insertions(+), 43 deletions(-) create mode 100644 src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/extension/ListExtension.kt diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/extension/ListExtension.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/extension/ListExtension.kt new file mode 100644 index 00000000..112195b1 --- /dev/null +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/extension/ListExtension.kt @@ -0,0 +1,12 @@ +package com.sunnychung.application.multiplatform.hellohttp.extension + +/** + * Can only be used on a **sorted** list. + * + * @param comparison This function should never return 0 + */ +fun List.binarySearchForInsertionPoint(comparison: (T) -> Int): Int { + val r = binarySearch(comparison = comparison) + if (r >= 0) throw IllegalArgumentException("Parameter `comparison` should never return 0") + return -(r + 1) +} diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt index 42ab9caa..0b41f1fa 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt @@ -1,10 +1,14 @@ package com.sunnychung.application.multiplatform.hellohttp.ux +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.VerticalScrollbar +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState @@ -20,6 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester @@ -34,6 +39,7 @@ import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily @@ -41,6 +47,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import com.sunnychung.application.multiplatform.hellohttp.extension.binarySearchForInsertionPoint import com.sunnychung.application.multiplatform.hellohttp.extension.contains import com.sunnychung.application.multiplatform.hellohttp.extension.insert import com.sunnychung.application.multiplatform.hellohttp.util.log @@ -82,10 +89,14 @@ fun CodeEditorView( var textValue by remember { mutableStateOf(TextFieldValue(text = text.filterForTextField())) } var cursorDelta by remember { mutableStateOf(0) } + var textLayoutResult by remember { mutableStateOf(null) } + var lineTops by remember { mutableStateOf?>(null) } val newText = text.filterForTextField() if (newText != textValue.text) { log.d { "CodeEditorView replace text len ${textValue.text.length} -> ${newText.length}" } textValue = textValue.copy(text = newText) + lineTops = null // recalculate + textLayoutResult = null } if (cursorDelta > 0) { textValue = textValue.copy( @@ -99,6 +110,14 @@ fun CodeEditorView( log.d { "CodeEditorView recompose" } + if (lineTops == null && textLayoutResult != null) { + log.d { "lineTops recalc start" } + val lineOffsets = listOf(0) + "\n".toRegex().findAll(textValue.text).map { it.range.endInclusive + 1 } + lineTops = lineOffsets.map { textLayoutResult!!.getLineTop(textLayoutResult!!.getLineForOffset(it)) } + // O(l * L * 1) + (Float.POSITIVE_INFINITY) + log.d { "lineTops recalc end" } + } + fun onPressEnterAddIndent() { val cursorPos = textValue.selection.min assert(textValue.selection.length == 0) @@ -199,7 +218,6 @@ fun CodeEditorView( var searchResultViewIndex by remember { mutableStateOf(0) } var lastSearchResultViewIndex by remember { mutableStateOf(0) } var searchResultRanges by rememberLast(searchText, searchOptions) { mutableStateOf?>(null) } - var textLayoutResult by remember { mutableStateOf(null) } var textFieldSize by remember { mutableStateOf(null) } if (searchText.isNotEmpty() && searchResultRanges == null) { @@ -236,6 +254,12 @@ fun CodeEditorView( emptyList() } + textLayoutResult?.let { tl -> + (0..minOf(10, tl.lineCount - 1)).forEach { + log.d { "> TL Line $it top=${tl.getLineTop(it)} bottom=${tl.getLineBottom(it)} h=${tl.getLineBottom(it) - tl.getLineTop(it)}" } + } + } + if (isSearchVisible) { if (!searchResultRanges.isNullOrEmpty()) { visualTransformations += SearchHighlightTransformation( @@ -327,54 +351,62 @@ fun CodeEditorView( } Box(modifier = Modifier.weight(1f).onGloballyPositioned { textFieldSize = it.size }) { // log.v { "CodeEditorView text=$text" } - AppTextField( - value = textValue, - onValueChange = { - textValue = it - log.d { "CEV sel ${textValue.selection.start}" } - onTextChange?.invoke(it.text) - }, - visualTransformation = visualTransformations.let { - if (it.size > 1) { - MultipleVisualTransformation(it) - } else if (it.size == 1) { - it.first() - } else { - VisualTransformation.None - } - }, - readOnly = isReadOnly, - textStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace), - colors = colors, - onTextLayout = { textLayoutResult = it }, - modifier = Modifier.fillMaxSize().verticalScroll(scrollState) - .focusRequester(textFieldFocusRequester) - .run { - if (!isReadOnly) { - this.onPreviewKeyEvent { - if (it.type == KeyEventType.KeyDown) { - when (it.key) { - Key.Enter -> { - onPressEnterAddIndent() - true - } + Row { + LineNumbersView( + scrollState = scrollState, + textLayoutResult = textLayoutResult, + lineTops = lineTops, + modifier = Modifier.fillMaxHeight(), + ) + AppTextField( + value = textValue, + onValueChange = { + textValue = it + log.d { "CEV sel ${textValue.selection.start}" } + onTextChange?.invoke(it.text) + }, + visualTransformation = visualTransformations.let { + if (it.size > 1) { + MultipleVisualTransformation(it) + } else if (it.size == 1) { + it.first() + } else { + VisualTransformation.None + } + }, + readOnly = isReadOnly, + textStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace), + colors = colors, + onTextLayout = { textLayoutResult = it }, + modifier = Modifier.fillMaxSize().verticalScroll(scrollState) + .focusRequester(textFieldFocusRequester) + .run { + if (!isReadOnly) { + this.onPreviewKeyEvent { + if (it.type == KeyEventType.KeyDown) { + when (it.key) { + Key.Enter -> { + onPressEnterAddIndent() + true + } - Key.Tab -> { - onPressTab(it.isShiftPressed) - true - } + Key.Tab -> { + onPressTab(it.isShiftPressed) + true + } - else -> false + else -> false + } + } else { + false } - } else { - false } + } else { + this } - } else { - this } - } - ) + ) + } VerticalScrollbar( modifier = Modifier.align(Alignment.CenterEnd), adapter = rememberScrollbarAdapter(scrollState), @@ -449,6 +481,39 @@ data class SearchOptions( val isWholeWord: Boolean, // ignore if isRegex is true ) +@Composable +fun LineNumbersView(modifier: Modifier = Modifier, scrollState: ScrollState, textLayoutResult: TextLayoutResult?, lineTops: List?) = with(LocalDensity.current) { + var size by remember { mutableStateOf(null) } + + Box( + modifier = modifier + .width(20.dp) + .fillMaxHeight() + .clipToBounds() + .onGloballyPositioned { size = it.size } + .background(LocalColor.current.backgroundLight) + .padding(top = 6.dp), // see AppTextField + ) { + if (size != null && textLayoutResult != null && lineTops != null) { + val viewportTop = scrollState.value.toFloat() + val viewportBottom = viewportTop + size!!.height + log.d { "LineNumbersView before calculation" } + // 0-based line index + val firstLine = lineTops.binarySearchForInsertionPoint { if (it <= viewportTop) -1 else 1 } - 1 + val lastLine = lineTops.binarySearchForInsertionPoint { if (it > viewportBottom) 1 else -1 } + log.v { "LineNumbersView $firstLine ~ <$lastLine / $viewportTop ~ $viewportBottom" } + log.v { "lineTops = $lineTops" } + log.d { "LineNumbersView after calculation" } + for (i in firstLine until minOf(lastLine, lineTops.size - 1)) { + AppText( + text = "${i + 1}", + modifier = Modifier.offset(y = (lineTops[i] - viewportTop).toDp()) + ) + } + } + } +} + fun getLineStart(text: String, position: Int): Int { for (i in (position - 1) downTo 0) { if (text[i] == '\n') { From a536047efd55e2e5c83c0971a460ef3a40e54a28 Mon Sep 17 00:00:00 2001 From: Sunny Chung Date: Sun, 11 Feb 2024 18:14:30 +0800 Subject: [PATCH 2/8] fix merge conflict --- .../application/multiplatform/hellohttp/ux/CodeEditorView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt index 840675eb..13ddd98e 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt @@ -220,7 +220,6 @@ fun CodeEditorView( var searchResultViewIndex by rememberLast(text) { mutableStateOf(0) } var lastSearchResultViewIndex by rememberLast(text) { mutableStateOf(0) } var searchResultRanges by rememberLast(text, searchPattern) { mutableStateOf?>(null) } - var textLayoutResult by remember { mutableStateOf(null) } var textFieldSize by remember { mutableStateOf(null) } if (searchText.isNotEmpty() && searchPattern == null) { From ebf215ec88dc340f0d7a443883ea854ca90ed9c2 Mon Sep 17 00:00:00 2001 From: Sunny Chung Date: Sun, 11 Feb 2024 18:51:59 +0800 Subject: [PATCH 3/8] fix line numbers were wrong if there is an offset in text --- .../hellohttp/ux/CodeEditorView.kt | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt index 13ddd98e..450f152e 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt @@ -111,14 +111,6 @@ fun CodeEditorView( log.d { "CodeEditorView recompose" } - if (lineTops == null && textLayoutResult != null) { - log.d { "lineTops recalc start" } - val lineOffsets = listOf(0) + "\n".toRegex().findAll(textValue.text).map { it.range.endInclusive + 1 } - lineTops = lineOffsets.map { textLayoutResult!!.getLineTop(textLayoutResult!!.getLineForOffset(it)) } + // O(l * L * 1) - (Float.POSITIVE_INFINITY) - log.d { "lineTops recalc end" } - } - fun onPressEnterAddIndent() { val cursorPos = textValue.selection.min assert(textValue.selection.length == 0) @@ -314,6 +306,28 @@ fun CodeEditorView( searchResultViewIndex = (searchResultViewIndex - 1 + size) % size } + val visualTransformationToUse = visualTransformations.let { + if (it.size > 1) { + MultipleVisualTransformation(it) + } else if (it.size == 1) { + it.first() + } else { + VisualTransformation.None + } + } + + log.d { "lineTops ${lineTops != null}, textLayoutResult ${textLayoutResult != null}" } + + if (lineTops == null && textLayoutResult != null) { + log.d { "lineTops recalc start" } + val charOffsetMapping = visualTransformationToUse.filter(AnnotatedString(textValue.text)).offsetMapping + val lineOffsets = listOf(0) + "\n".toRegex().findAll(textValue.text).map { charOffsetMapping.originalToTransformed(it.range.endInclusive + 1) } + log.v { "lineOffsets = $lineOffsets" } + lineTops = lineOffsets.map { textLayoutResult!!.getLineTop(textLayoutResult!!.getLineForOffset(it)) } + // O(l * L * 1) + (Float.POSITIVE_INFINITY) + log.d { "lineTops recalc end" } + } + Column(modifier = modifier.onPreviewKeyEvent { if (it.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false if (it.key == Key.F && (it.isMetaPressed || it.isCtrlPressed)) { @@ -381,15 +395,7 @@ fun CodeEditorView( log.d { "CEV sel ${textValue.selection.start}" } onTextChange?.invoke(it.text) }, - visualTransformation = visualTransformations.let { - if (it.size > 1) { - MultipleVisualTransformation(it) - } else if (it.size == 1) { - it.first() - } else { - VisualTransformation.None - } - }, + visualTransformation = visualTransformationToUse, readOnly = isReadOnly, textStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace), colors = colors, From c441f0f8802a60ffc9ec45d4ee9d5b2473e3aed5 Mon Sep 17 00:00:00 2001 From: Sunny Chung Date: Sun, 11 Feb 2024 18:52:22 +0800 Subject: [PATCH 4/8] fix pasting text did not update line numbers --- .../application/multiplatform/hellohttp/ux/CodeEditorView.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt index 450f152e..4127d145 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt @@ -90,9 +90,10 @@ fun CodeEditorView( var textValue by remember { mutableStateOf(TextFieldValue(text = text.filterForTextField())) } var cursorDelta by remember { mutableStateOf(0) } - var textLayoutResult by remember { mutableStateOf(null) } - var lineTops by remember { mutableStateOf?>(null) } val newText = text.filterForTextField() + var textLayoutResult by rememberLast(newText) { mutableStateOf(null) } + var lineTops by rememberLast(newText) { mutableStateOf?>(null) } + log.d { "len newText ${newText.length}, textValue.text ${textValue.text.length}, text ${text.length}" } if (newText != textValue.text) { log.d { "CodeEditorView replace text len ${textValue.text.length} -> ${newText.length}" } textValue = textValue.copy(text = newText) From 40c840b4d98a615a3d3ff342389df4c7e462a2ac Mon Sep 17 00:00:00 2001 From: Sunny Chung Date: Sun, 11 Feb 2024 20:57:59 +0800 Subject: [PATCH 5/8] update line number display style --- .../hellohttp/ux/CodeEditorView.kt | 31 ++++++++++++++----- .../hellohttp/ux/local/AppColor.kt | 3 ++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt index 4127d145..7a70885b 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt @@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -46,8 +48,10 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.sunnychung.application.multiplatform.hellohttp.extension.binarySearchForInsertionPoint import com.sunnychung.application.multiplatform.hellohttp.extension.contains import com.sunnychung.application.multiplatform.hellohttp.extension.insert @@ -506,16 +510,17 @@ data class SearchOptions( @Composable fun LineNumbersView(modifier: Modifier = Modifier, scrollState: ScrollState, textLayoutResult: TextLayoutResult?, lineTops: List?) = with(LocalDensity.current) { + val colours = LocalColor.current var size by remember { mutableStateOf(null) } Box( modifier = modifier - .width(20.dp) + .width(28.dp) .fillMaxHeight() .clipToBounds() .onGloballyPositioned { size = it.size } - .background(LocalColor.current.backgroundLight) - .padding(top = 6.dp), // see AppTextField + .background(colours.backgroundLight) + .padding(top = 6.dp, end = 8.dp), // see AppTextField ) { if (size != null && textLayoutResult != null && lineTops != null) { val viewportTop = scrollState.value.toFloat() @@ -527,11 +532,23 @@ fun LineNumbersView(modifier: Modifier = Modifier, scrollState: ScrollState, tex log.v { "LineNumbersView $firstLine ~ <$lastLine / $viewportTop ~ $viewportBottom" } log.v { "lineTops = $lineTops" } log.d { "LineNumbersView after calculation" } + val lineHeight = textLayoutResult.getLineBottom(0) - textLayoutResult.getLineTop(0) for (i in firstLine until minOf(lastLine, lineTops.size - 1)) { - AppText( - text = "${i + 1}", - modifier = Modifier.offset(y = (lineTops[i] - viewportTop).toDp()) - ) + Box( + contentAlignment = Alignment.CenterEnd, + modifier = Modifier + .fillMaxWidth() + .height(lineHeight.toDp()) + .offset(y = (lineTops[i] - viewportTop).toDp()), + ) { + AppText( + text = "${i + 1}", + fontSize = 13.sp, + fontFamily = FontFamily.Monospace, + maxLines = 1, + color = colours.unimportant, + ) + } } } } diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/local/AppColor.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/local/AppColor.kt index 0b65b0b1..64507192 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/local/AppColor.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/local/AppColor.kt @@ -21,6 +21,7 @@ data class AppColor( val bright: Color, val successful: Color, val text: Color = primary, + val unimportant: Color, val image: Color = primary, val line: Color, @@ -95,6 +96,7 @@ fun darkColorScheme(): AppColor = AppColor( backgroundInputFieldHighlightEmphasize = Color(red = 0.6f, green = 0.38f, blue = 0f), primary = Color(red = 0.8f, green = 0.8f, blue = 1.0f), + unimportant = Color(red = 0.45f, green = 0.45f, blue = 0.65f), bright = Color.White, successful = Color(red = 0.1f, green = 0.8f, blue = 0.1f), line = Color(red = 0.6f, green = 0.6f, blue = 0.6f), @@ -164,6 +166,7 @@ fun lightColorScheme(): AppColor = AppColor( backgroundInputFieldHighlightEmphasize = Color(red = 0.8f, green = 0.8f, blue = 0.3f), primary = Color(red = 0.2f, green = 0.2f, blue = 0.3f), + unimportant = Color(red = 0.4f, green = 0.4f, blue = 0.5f), bright = Color.Black, successful = Color(red = 0.1f, green = 0.6f, blue = 0.1f), line = Color(red = 0.4f, green = 0.4f, blue = 0.4f), From deec62799acb3cfe867cd05c8164496f144d4913 Mon Sep 17 00:00:00 2001 From: Sunny Chung Date: Sun, 11 Feb 2024 21:51:59 +0800 Subject: [PATCH 6/8] update line numbers to adjust width according to maximum number of digits --- .../hellohttp/ux/CodeEditorView.kt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt index 7a70885b..82131e74 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp @@ -512,15 +513,26 @@ data class SearchOptions( fun LineNumbersView(modifier: Modifier = Modifier, scrollState: ScrollState, textLayoutResult: TextLayoutResult?, lineTops: List?) = with(LocalDensity.current) { val colours = LocalColor.current var size by remember { mutableStateOf(null) } + val textMeasurer = rememberTextMeasurer() + val textStyle = LocalTextStyle.current.copy( + fontSize = 13.sp, + fontFamily = FontFamily.Monospace, + color = colours.unimportant, + ) + val lineNumDigits = "${(lineTops?.size ?: 2) - 1}".length + val width = rememberLast(lineNumDigits) { + maxOf(textMeasurer.measure("8".repeat(lineNumDigits), textStyle, maxLines = 1).size.width.toDp(), 20.dp) + + 4.dp + 8.dp + } Box( modifier = modifier - .width(28.dp) + .width(width) .fillMaxHeight() .clipToBounds() .onGloballyPositioned { size = it.size } .background(colours.backgroundLight) - .padding(top = 6.dp, end = 8.dp), // see AppTextField + .padding(top = 6.dp, end = 8.dp, start = 4.dp), // see AppTextField ) { if (size != null && textLayoutResult != null && lineTops != null) { val viewportTop = scrollState.value.toFloat() @@ -543,6 +555,7 @@ fun LineNumbersView(modifier: Modifier = Modifier, scrollState: ScrollState, tex ) { AppText( text = "${i + 1}", + style = textStyle, fontSize = 13.sp, fontFamily = FontFamily.Monospace, maxLines = 1, From 31f4f64bd63405245db4405a24bbd66dc3ba6aff Mon Sep 17 00:00:00 2001 From: Sunny Chung Date: Sun, 11 Feb 2024 21:52:14 +0800 Subject: [PATCH 7/8] fix incorrect line number positions --- .../application/multiplatform/hellohttp/ux/CodeEditorView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt index 82131e74..29961003 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt @@ -97,7 +97,7 @@ fun CodeEditorView( var cursorDelta by remember { mutableStateOf(0) } val newText = text.filterForTextField() var textLayoutResult by rememberLast(newText) { mutableStateOf(null) } - var lineTops by rememberLast(newText) { mutableStateOf?>(null) } + var lineTops by rememberLast(newText, textLayoutResult) { mutableStateOf?>(null) } log.d { "len newText ${newText.length}, textValue.text ${textValue.text.length}, text ${text.length}" } if (newText != textValue.text) { log.d { "CodeEditorView replace text len ${textValue.text.length} -> ${newText.length}" } From 2622cca8b3e9be0ff90bc4eb1105e1536cfcaba6 Mon Sep 17 00:00:00 2001 From: Sunny Chung Date: Sun, 11 Feb 2024 22:03:36 +0800 Subject: [PATCH 8/8] fix line numbers were blinking on typing --- .../multiplatform/hellohttp/ux/CodeEditorView.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt index 29961003..71bc4295 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/CodeEditorView.kt @@ -519,7 +519,17 @@ fun LineNumbersView(modifier: Modifier = Modifier, scrollState: ScrollState, tex fontFamily = FontFamily.Monospace, color = colours.unimportant, ) - val lineNumDigits = "${(lineTops?.size ?: 2) - 1}".length + log.v { "LineNumbersView ${size != null} && ${textLayoutResult != null} && ${lineTops != null}" } + var lastTextLayoutResult by remember { mutableStateOf(textLayoutResult) } + var lastLineTops by remember { mutableStateOf(lineTops) } + + val textLayoutResult = textLayoutResult ?: lastTextLayoutResult + val lineTops = lineTops ?: lastLineTops + + lastTextLayoutResult = textLayoutResult + lastLineTops = lineTops + + val lineNumDigits = lineTops?.let { "${it.lastIndex}".length } ?: 1 val width = rememberLast(lineNumDigits) { maxOf(textMeasurer.measure("8".repeat(lineNumDigits), textStyle, maxLines = 1).size.width.toDp(), 20.dp) + 4.dp + 8.dp