From 3be531a76f06ada4a8845791c91f3ebd755e4a64 Mon Sep 17 00:00:00 2001 From: EpicDima Date: Sat, 18 Nov 2023 18:11:50 +0300 Subject: [PATCH] Change the handling of out-of-bounds canvas --- .../kotlin/com/jakewharton/mosaic/canvas.kt | 32 +- .../jakewharton/mosaic/layout/OffsetTest.kt | 370 +++++++++++++++--- 2 files changed, 336 insertions(+), 66 deletions(-) diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt index dab03c98..ef45964b 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt @@ -23,8 +23,6 @@ internal interface TextCanvas { operator fun get(row: Int, column: Int): TextPixel } -private val blankPixel = TextPixel(' ') - internal class TextSurface( override val width: Int, override val height: Int, @@ -33,16 +31,23 @@ internal class TextSurface( override var translationX = 0 override var translationY = 0 - private val rows = Array(height) { Array(width) { TextPixel(' ') } } + private val rows = Array(height) { Array(width) { newBlankPixel } } - override operator fun get(row: Int, column: Int) = rows[translationY + row][translationX + column] + override operator fun get(row: Int, column: Int): TextPixel { + val x = translationX + column + val y = translationY + row + if (x >= width || y >= height || x < 0 || y < 0) { + return reusableDirtyPixel + } + return rows[y][x] + } fun appendRowTo(appendable: Appendable, row: Int) { // Reused heap allocation for building ANSI attributes inside the loop. val attributes = mutableListOf() val rowPixels = rows[row] - var lastPixel = blankPixel + var lastPixel = reusableBlankPixel for (columnIndex in 0 until width) { val pixel = rowPixels[columnIndex] @@ -153,6 +158,23 @@ internal class TextSurface( } } +/** + * Returns always a new blank [TextPixel]. + */ +private val newBlankPixel: TextPixel get() = TextPixel(' ') + +/** + * It is used in places where it is important that the [TextPixel] + * has its original state and **will not change**. + */ +private val reusableBlankPixel: TextPixel = newBlankPixel + +/** + * It is used in places where the [TextPixel] state is not important + * and it can change. + */ +private val reusableDirtyPixel: TextPixel = newBlankPixel + internal class TextPixel(var codePoint: Int) { var background: Color = Color.Unspecified var foreground: Color = Color.Unspecified diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/layout/OffsetTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/layout/OffsetTest.kt index 03c45b5a..c15759d6 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/layout/OffsetTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/layout/OffsetTest.kt @@ -11,7 +11,6 @@ import com.jakewharton.mosaic.s import com.jakewharton.mosaic.ui.Box import com.jakewharton.mosaic.ui.unit.IntOffset import kotlin.test.Test -import kotlin.test.assertFails class OffsetTest { @Test fun offsetHorizontalFixed() { @@ -33,24 +32,64 @@ class OffsetTest { ) } + @Test fun offsetHorizontalFixedFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.requiredSize(size).offset(30, 0)) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + + @Test fun offsetHorizontalFixedNegativeFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset(-30, 0)) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + @Test fun offsetHorizontalFixedBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset(30, 0)) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.requiredSize(size).offset(4, 0)) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + | $TestChar$TestChar + | $TestChar$TestChar + | $TestChar$TestChar + | $s + | $s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetHorizontalFixedNegativeBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset(-3, 0)) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset(-1, 0)) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + |$TestChar$TestChar $s + |$TestChar$TestChar $s + |$TestChar$TestChar $s + | $s + | $s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetVerticalFixed() { @@ -72,24 +111,64 @@ class OffsetTest { ) } + @Test fun offsetVerticalFixedFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset(0, 40)) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + + @Test fun offsetVerticalFixedNegativeFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset(0, -40)) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + @Test fun offsetVerticalFixedBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset(0, 40)) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset(0, 4)) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + | $s + | $s + | $s + | $s + |$TestChar$TestChar$TestChar $s + |$TestChar$TestChar$TestChar $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetVerticalFixedNegativeBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset(0, -4)) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset(0, -1)) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + |$TestChar$TestChar$TestChar $s + |$TestChar$TestChar$TestChar $s + | $s + | $s + | $s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetFixed() { @@ -111,24 +190,64 @@ class OffsetTest { ) } + @Test fun offsetFixedFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset(30, 40)) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + + @Test fun offsetFixedNegativeFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset(-30, -40)) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + @Test fun offsetFixedBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset(30, 40)) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset(4, 5)) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + | $s + | $s + | $s + | $s + | $s + | $TestChar$TestChar + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetFixedNegativeBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset(-3, -4)) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset(-1, -2)) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + |$TestChar$TestChar $s + | $s + | $s + | $s + | $s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetFixedDebug() { @@ -155,24 +274,64 @@ class OffsetTest { ) } + @Test fun offsetHorizontalModifiableFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(30, 0) }) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + + @Test fun offsetHorizontalModifiableNegativeFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(-30, 0) }) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + @Test fun offsetHorizontalModifiableBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset { IntOffset(30, 0) }) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(4, 0) }) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + | $TestChar$TestChar + | $TestChar$TestChar + | $TestChar$TestChar + | $s + | $s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetHorizontalModifiableNegativeBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset { IntOffset(-3, 0) }) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(-1, 0) }) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + |$TestChar$TestChar $s + |$TestChar$TestChar $s + |$TestChar$TestChar $s + | $s + | $s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetVerticalModifiable() { @@ -194,24 +353,64 @@ class OffsetTest { ) } + @Test fun offsetVerticalModifiableFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(0, 40) }) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + + @Test fun offsetVerticalModifiableNegativeFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(0, -40) }) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + @Test fun offsetVerticalModifiableBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset { IntOffset(0, 40) }) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(0, 4) }) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + | $s + | $s + | $s + | $s + |$TestChar$TestChar$TestChar $s + |$TestChar$TestChar$TestChar $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetVerticalModifiableNegativeBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset { IntOffset(0, -4) }) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(0, -1) }) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + |$TestChar$TestChar$TestChar $s + |$TestChar$TestChar$TestChar $s + | $s + | $s + | $s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetModifiable() { @@ -233,24 +432,64 @@ class OffsetTest { ) } + @Test fun offsetModifiableFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(30, 40) }) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + + @Test fun offsetModifiableNegativeFarBeyondBorders() { + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(-30, -40) }) { + TestFiller(modifier = Modifier.size(1)) + } + } + assertThat(actual).isEqualTo(getBlankStringBlock(size)) + } + @Test fun offsetModifiableBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset { IntOffset(30, 40) }) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(4, 5) }) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + | $s + | $s + | $s + | $s + | $s + | $TestChar$TestChar + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetModifiableNegativeBeyondBorders() { - assertFails { - renderMosaic { - Box(modifier = Modifier.size(6).offset { IntOffset(-3, -4) }) { - TestFiller(modifier = Modifier.size(1)) - } + val size = 6 + val actual = renderMosaic { + Box(modifier = Modifier.size(size).offset { IntOffset(-1, -2) }) { + TestFiller(modifier = Modifier.size(3)) } } + assertThat(actual).isEqualTo( + """ + |$TestChar$TestChar $s + | $s + | $s + | $s + | $s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) } @Test fun offsetModifiableDebug() { @@ -258,4 +497,13 @@ class OffsetTest { val actual = Modifier.offset(offsetLambda).toString() assertThat(actual).isEqualTo("ChangeableOffset(offset=$offsetLambda)") } + + private fun getBlankStringBlock(size: Int): String { + val line = s.repeat(size) + return buildString { + repeat(size) { + appendLine(line) + } + }.replaceLineEndingsWithCRLF() + } }