diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skia/paragraph/ParagraphStyle.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skia/paragraph/ParagraphStyle.kt index cfc57a659..d01288d38 100644 --- a/skiko/src/commonMain/kotlin/org/jetbrains/skia/paragraph/ParagraphStyle.kt +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skia/paragraph/ParagraphStyle.kt @@ -191,6 +191,19 @@ class ParagraphStyle : Managed(ParagraphStyle_nMake(), _FinalizerHolder.PTR) { reachabilityBarrier(this) } + var isApplyRoundingHackEnabled: Boolean + get() = try { + Stats.onNativeCall() + _nGetApplyRoundingHack(_ptr).not().not() + } finally { + reachabilityBarrier(this) + } + set(value) = try { + Stats.onNativeCall() + _nSetApplyRoundingHack(_ptr, value) + } finally { + reachabilityBarrier(this) + } var textIndent: TextIndent get() = try { @@ -322,6 +335,14 @@ private external fun _nGetHinting(ptr: NativePointer): Int @ModuleImport("./skiko.mjs", "org_jetbrains_skia_paragraph_ParagraphStyle__1nGetSubpixel") private external fun _nGetSubpixel(ptr: NativePointer): Boolean +@ExternalSymbolName("org_jetbrains_skia_paragraph_ParagraphStyle__1nGetApplyRoundingHack") +@ModuleImport("./skiko.mjs", "org_jetbrains_skia_paragraph_ParagraphStyle__1nGetApplyRoundingHack") +private external fun _nGetApplyRoundingHack(ptr: NativePointer): Boolean + +@ExternalSymbolName("org_jetbrains_skia_paragraph_ParagraphStyle__1nSetApplyRoundingHack") +@ModuleImport("./skiko.mjs", "org_jetbrains_skia_paragraph_ParagraphStyle__1nSetApplyRoundingHack") +private external fun _nSetApplyRoundingHack(ptr: NativePointer, value: Boolean) + @ExternalSymbolName("org_jetbrains_skia_paragraph_ParagraphStyle__1nSetTextIndent") @ModuleImport("./skiko.mjs", "org_jetbrains_skia_paragraph_ParagraphStyle__1nSetTextIndent") private external fun _nSetTextIndent(ptr: NativePointer, firstLine: Float, restLine: Float) diff --git a/skiko/src/commonTest/kotlin/org/jetbrains/skia/ParagraphTest.kt b/skiko/src/commonTest/kotlin/org/jetbrains/skia/ParagraphTest.kt index 40ab5ae68..6437f1129 100644 --- a/skiko/src/commonTest/kotlin/org/jetbrains/skia/ParagraphTest.kt +++ b/skiko/src/commonTest/kotlin/org/jetbrains/skia/ParagraphTest.kt @@ -6,6 +6,9 @@ import org.jetbrains.skia.tests.assertCloseEnough import org.jetbrains.skia.tests.assertContentCloseEnough import org.jetbrains.skia.tests.makeFromResource import org.jetbrains.skiko.tests.* +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.truncate import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals @@ -15,6 +18,7 @@ class ParagraphTest { private val fontCollection = suspend { FontCollection().setDefaultFontManager(TypefaceFontProvider().apply { registerTypeface(Typeface.makeFromResource("./fonts/Inter-Hinted-Regular.ttf"), "Inter") + registerTypeface(Typeface.makeFromResource("./fonts/JetBrainsMono_2_304/JetBrainsMono-Regular.ttf"), "JetBrains Mono") }) } private val style = ParagraphStyle().apply { @@ -47,40 +51,43 @@ class ParagraphTest { @Test fun layoutParagraph() = runTest { - val lineMetricsEpsilon = 0.0001f + val lineMetricsEpsilon = 0.001f assertCloseEnough( - singleLineMetrics("aa"), LineMetrics( + actual = singleLineMetrics("aa"), + expected = LineMetrics( startIndex = 0, endIndex = 2, endExcludingWhitespaces = 2, endIncludingNewline = 2, isHardBreak = true, ascent = 13.5625, - descent = 3.3806817531585693, + descent = 3.380584716796875, unscaledAscent = 13.5625, height = 17.0, - width = 15.789999961853027, + width = 15.789764404296875, left = 0.0, - baseline = 13.619318008422852, + baseline = 13.619415283203125, lineNumber = 0 ), epsilon = lineMetricsEpsilon ) + assertCloseEnough( - singleLineMetrics("яя"), LineMetrics( + actual = singleLineMetrics("яя"), + expected = LineMetrics( startIndex = 0, endIndex = 2, endExcludingWhitespaces = 2, endIncludingNewline = 2, isHardBreak = true, ascent = 13.5625, - descent = 3.3806817531585693, + descent = 3.380584716796875, unscaledAscent = 13.5625, height = 17.0, - width = 15.710000038146973, + width = 15.710235595703125, left = 0.0, - baseline = 13.619318008422852, + baseline = 13.619415283203125, lineNumber = 0 ), epsilon = lineMetricsEpsilon ) @@ -172,4 +179,35 @@ class ParagraphTest { } } } + + @Test + fun layout_paragraph_with_its_maxIntrinsicWidth_shouldnt_lead_to_wraps() = runTest { + suspend fun testWraps(isApplyRoundingHackEnabled: Boolean, unexpectedWrapsPresent: Boolean) { + val paragraphStyle = ParagraphStyle().apply { + this.isApplyRoundingHackEnabled = isApplyRoundingHackEnabled + textStyle = TextStyle().apply { + fontFamilies = arrayOf("JetBrains Mono") + fontSize = 13.0f * 2f + } + } + val paragraph = ParagraphBuilder(paragraphStyle, fontCollection()).use { + it.addText("x".repeat(104)) + it.addText(" ") + it.addText("y".repeat(100)) + it.build() + }.layout(Float.POSITIVE_INFINITY) + assertEquals(1, paragraph.lineNumber, "Layout in one line with Inf width") + + val maxIntrinsicWidth = paragraph.maxIntrinsicWidth + val expectedLines = if (unexpectedWrapsPresent) 2 else 1 + + paragraph.layout(paragraph.maxIntrinsicWidth) + assertEquals(expectedLines, paragraph.lineNumber, "Layout with maxIntrinsicWidth " + + "maxIntrinsicWidth: $maxIntrinsicWidth " + + "unexpectedWrapsPresent: $unexpectedWrapsPresent " + + "isApplyRoundingHackEnabled: $isApplyRoundingHackEnabled") + } + testWraps(isApplyRoundingHackEnabled = false, unexpectedWrapsPresent = false) + testWraps(isApplyRoundingHackEnabled = true, unexpectedWrapsPresent = true) + } } diff --git a/skiko/src/commonTest/kotlin/org/jetbrains/skiko/paragraph/ParagraphStyleTests.kt b/skiko/src/commonTest/kotlin/org/jetbrains/skiko/paragraph/ParagraphStyleTests.kt index ecde4464e..b94dc6fcb 100644 --- a/skiko/src/commonTest/kotlin/org/jetbrains/skiko/paragraph/ParagraphStyleTests.kt +++ b/skiko/src/commonTest/kotlin/org/jetbrains/skiko/paragraph/ParagraphStyleTests.kt @@ -55,4 +55,13 @@ class ParagraphStyleTests { assertEquals(gloriousRasterSettings, paragraphStyle.fontRastrSettings) } } + + @Test + fun paragraphStyleRoundingHackTests() { + ParagraphStyle().use { paragraphStyle -> + assertEquals(false, paragraphStyle.isApplyRoundingHackEnabled) + paragraphStyle.isApplyRoundingHackEnabled = true + assertEquals(true, paragraphStyle.isApplyRoundingHackEnabled) + } + } } \ No newline at end of file diff --git a/skiko/src/commonTest/resources/fonts/JetBrainsMono_2_304/JetBrainsMono-Regular.ttf b/skiko/src/commonTest/resources/fonts/JetBrainsMono_2_304/JetBrainsMono-Regular.ttf new file mode 100644 index 000000000..dff66cc50 Binary files /dev/null and b/skiko/src/commonTest/resources/fonts/JetBrainsMono_2_304/JetBrainsMono-Regular.ttf differ diff --git a/skiko/src/jvmMain/cpp/common/paragraph/ParagraphStyle.cc b/skiko/src/jvmMain/cpp/common/paragraph/ParagraphStyle.cc index 1efca656f..a6f249d25 100644 --- a/skiko/src/jvmMain/cpp/common/paragraph/ParagraphStyle.cc +++ b/skiko/src/jvmMain/cpp/common/paragraph/ParagraphStyle.cc @@ -19,6 +19,7 @@ extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_paragraph_ParagraphSt extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_paragraph_ParagraphStyleKt_ParagraphStyle_1nMake (JNIEnv* env, jclass jclass) { ParagraphStyle* instance = new ParagraphStyle(); + instance->setApplyRoundingHack(false); return reinterpret_cast(instance); } @@ -180,6 +181,18 @@ extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_paragraph_ParagraphSty instance->turnHintingOff(); } +extern "C" JNIEXPORT jboolean JNICALL Java_org_jetbrains_skia_paragraph_ParagraphStyleKt__1nGetApplyRoundingHack + (JNIEnv* env, jclass jclass, jlong ptr) { + ParagraphStyle* instance = reinterpret_cast(static_cast(ptr)); + return instance->getApplyRoundingHack(); +} + +extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_paragraph_ParagraphStyleKt__1nSetApplyRoundingHack + (JNIEnv* env, jclass jclass, jlong ptr, jboolean val) { + ParagraphStyle* instance = reinterpret_cast(static_cast(ptr)); + instance->setApplyRoundingHack(val); +} + extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_paragraph_ParagraphStyleKt__1nSetTextIndent (JNIEnv* env, jclass jclass, jlong ptr, jfloat firstLine, jfloat restLine) { ParagraphStyle* instance = reinterpret_cast(static_cast(ptr)); diff --git a/skiko/src/nativeJsMain/cpp/paragraph/ParagraphStyle.cc b/skiko/src/nativeJsMain/cpp/paragraph/ParagraphStyle.cc index 5d938a29c..9c23af9ec 100644 --- a/skiko/src/nativeJsMain/cpp/paragraph/ParagraphStyle.cc +++ b/skiko/src/nativeJsMain/cpp/paragraph/ParagraphStyle.cc @@ -16,6 +16,7 @@ SKIKO_EXPORT KNativePointer org_jetbrains_skia_paragraph_ParagraphStyle__1nGetFi SKIKO_EXPORT KNativePointer org_jetbrains_skia_paragraph_ParagraphStyle__1nMake () { ParagraphStyle* instance = new ParagraphStyle(); + instance->setApplyRoundingHack(false); return reinterpret_cast(instance); } @@ -177,6 +178,18 @@ SKIKO_EXPORT KBoolean org_jetbrains_skia_paragraph_ParagraphStyle__1nGetSubpixel return fontRastrSettings.fSubpixel; } +SKIKO_EXPORT KBoolean org_jetbrains_skia_paragraph_ParagraphStyle__1nGetApplyRoundingHack + (KNativePointer ptr) { + ParagraphStyle* instance = reinterpret_cast(ptr); + return instance->getApplyRoundingHack(); +} + +SKIKO_EXPORT void org_jetbrains_skia_paragraph_ParagraphStyle__1nSetApplyRoundingHack + (KNativePointer ptr, KBoolean val) { + ParagraphStyle* instance = reinterpret_cast(ptr); + instance->setApplyRoundingHack(val); +} + SKIKO_EXPORT void org_jetbrains_skia_paragraph_ParagraphStyle__1nSetTextIndent (KNativePointer ptr, KFloat firstLine, KFloat restLine) { ParagraphStyle* instance = reinterpret_cast((ptr));