Skip to content

Commit b91caee

Browse files
committed
fix(view): keep the horizontal scroll when a prop repeats
Nitro resends an object prop whenever its JavaScript identity changes, and an inline literal is a new object on every render. The example Editor screen re-renders on each scroll event for its status line, so contentInsets reached the native setter per frame, relayout zeroed the widest line and the clamp snapped scrollX to 0: a horizontal swipe flickered between the finger and column 0. The hybrid now ignores theme and contentInsets values equal to the current ones, relayout measures the visible lines again and restores the horizontal scroll, the example hoists its insets literal, and an instrumented test scrolls right, changes the insets and expects scrollX unchanged.
1 parent 03de8e6 commit b91caee

4 files changed

Lines changed: 51 additions & 1 deletion

File tree

‎android/src/main/java/com/margelo/nitro/codepad/HybridCodeEditorView.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class HybridCodeEditorView(val context: ThemedReactContext) : HybridCodeEditorVi
5656

5757
override var theme: NativeTheme? = null
5858
set(value) {
59+
if (value == field) return
5960
field = value
6061
editor.setTheme(value?.let { toColors(it) } ?: ThemeColors.default)
6162
}
@@ -116,6 +117,7 @@ class HybridCodeEditorView(val context: ThemedReactContext) : HybridCodeEditorVi
116117

117118
override var contentInsets: NativeInsets? = null
118119
set(value) {
120+
if (value == field) return
119121
field = value
120122
editor.setContentInsets(
121123
(value?.top ?: 0.0).toFloat(),

‎android/src/main/java/me/sectiontn/codepad/view/CodeEditorView.kt‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,15 +452,27 @@ class CodeEditorView(context: Context) : View(context) {
452452
private fun relayout() {
453453
anchorDirty = true
454454
val anchor = viewport.anchor()
455+
val scrollX = viewport.scrollX
455456
viewport.rowHeight = rowMetrics().rowHeight
456457
cache.clear()
457458
heights.resetAll()
458459
viewport.widestLine = 0
459460
updateGutter()
460461
viewport.restore(anchor)
462+
// The visible lines are measured again right away so the horizontal position survives the widest line reset.
463+
layoutVisibleLines()
464+
viewport.scrollTo(scrollX, viewport.scrollY)
461465
invalidate()
462466
}
463467

468+
private fun layoutVisibleLines() {
469+
var line = viewport.firstVisibleLine()
470+
while (line < core.document.lineCount && line <= viewport.lastVisibleLine()) {
471+
if (cache[line] == null) layoutLine(line)
472+
line++
473+
}
474+
}
475+
464476
private fun updateGutter(): Boolean {
465477
val w = if (lineNumbers) {
466478
GutterMetrics.width(core.document.lineCount, paint.measureText("0"), dp(GUTTER_PADDING_DP).toFloat())
@@ -501,6 +513,7 @@ class CodeEditorView(context: Context) : View(context) {
501513
val entry = Entry(layout, null, -1)
502514
cache.put(line, entry)
503515
viewport.measured(line, layout.rowCount)
516+
if (!viewport.wrap) viewport.widestLine = maxOf(viewport.widestLine, Math.round(layout.width()))
504517
return entry
505518
}
506519

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package codepad.example
2+
3+
import android.os.SystemClock
4+
import android.view.KeyEvent
5+
import androidx.test.ext.junit.runners.AndroidJUnit4
6+
import org.junit.Assert.assertEquals
7+
import org.junit.Assert.assertTrue
8+
import org.junit.Test
9+
import org.junit.runner.RunWith
10+
11+
@RunWith(AndroidJUnit4::class)
12+
class ScrollTest {
13+
@Test
14+
fun insetChangesKeepTheHorizontalScroll() {
15+
launch().use { s ->
16+
val editor = s.editor()
17+
onMain {
18+
editor.setLineNumbers(false)
19+
editor.setInitialText("x".repeat(200))
20+
layout(editor)
21+
draw(editor)
22+
val now = SystemClock.uptimeMillis()
23+
editor.dispatchKeyEvent(KeyEvent(now, now, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MOVE_END, 0))
24+
draw(editor)
25+
val before = editor.frame.scrollX
26+
assertTrue("the caret reveal scrolled right, scrollX $before", before > 0f)
27+
editor.setContentInsets(0f, 24f, 0f, 0f)
28+
draw(editor)
29+
assertEquals("scrollX after the inset change", before, editor.frame.scrollX, 0.5f)
30+
}
31+
}
32+
}
33+
}

‎example/src/screens/EditorScreen.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const block = [
2424

2525
const sample = new Array(40).fill(block).join('\n');
2626

27+
// Object props keep one identity across renders; a fresh literal would reach native on every scroll event.
28+
const INSETS = { bottom: 24 };
2729
const SUGGESTIONS: Suggestions[] = ['off', 'forceOff', 'on'];
2830

2931
function Toggle({ label, value, onPress }: { label: string; value: boolean; onPress: () => void }) {
@@ -90,7 +92,7 @@ export function EditorScreen() {
9092
highlightActiveLine={activeLine}
9193
placeholder="Start typing"
9294
scrollEnabled
93-
contentInsets={{ bottom: 24 }}
95+
contentInsets={INSETS}
9496
suggestions={suggestions}
9597
autoCloseBrackets={autoClose}
9698
onFocus={() => setFocused(true)}

0 commit comments

Comments
 (0)