Unverified Commit 25ff8bee authored by Aleksandr Veselov's avatar Aleksandr Veselov Committed by GitHub

Implement Paragraph::getRectsForRange for native (#378)

parent 21a47543
package org.jetbrains.skia package org.jetbrains.skia
import org.jetbrains.skia.impl.InteropPointer
import org.jetbrains.skia.impl.InteropScope
import org.jetbrains.skia.impl.withResult
class IRange(val start: Int, val end: Int) { class IRange(val start: Int, val end: Int) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other === this) return true if (other === this) return true
...@@ -19,4 +23,11 @@ class IRange(val start: Int, val end: Int) { ...@@ -19,4 +23,11 @@ class IRange(val start: Int, val end: Int) {
override fun toString(): String { override fun toString(): String {
return "IRange(_start=$start, _end=$end)" return "IRange(_start=$start, _end=$end)"
} }
companion object {}
}
internal fun IRange.Companion.fromInteropPointer(block: InteropScope.(InteropPointer) -> Unit): IRange {
val result = withResult(IntArray(2), block)
return IRange(result[0], result[1])
} }
\ No newline at end of file
package org.jetbrains.skia.paragraph package org.jetbrains.skia.paragraph
import org.jetbrains.skia.impl.Library.Companion.staticLoad
import org.jetbrains.skia.* import org.jetbrains.skia.*
import org.jetbrains.skia.ExternalSymbolName
import org.jetbrains.skia.impl.* import org.jetbrains.skia.impl.*
import org.jetbrains.skia.impl.Library.Companion.staticLoad
class Paragraph internal constructor(ptr: NativePointer, text: ManagedString?) : Managed(ptr, _FinalizerHolder.PTR) { class Paragraph internal constructor(ptr: NativePointer, text: ManagedString?) : Managed(ptr, _FinalizerHolder.PTR) {
companion object { companion object {
...@@ -108,13 +107,15 @@ class Paragraph internal constructor(ptr: NativePointer, text: ManagedString?) : ...@@ -108,13 +107,15 @@ class Paragraph internal constructor(ptr: NativePointer, text: ManagedString?) :
): Array<TextBox> { ): Array<TextBox> {
return try { return try {
Stats.onNativeCall() Stats.onNativeCall()
_nGetRectsForRange(_ptr, start, end, rectHeightMode.ordinal, rectWidthMode.ordinal) interopScope {
_nGetRectsForRange(_ptr, start, end, rectHeightMode.ordinal, rectWidthMode.ordinal).fromInterop(TextBox)
}
} finally { } finally {
reachabilityBarrier(this) reachabilityBarrier(this)
} }
} }
val rectsForPlaceholders: Array<org.jetbrains.skia.paragraph.TextBox> val rectsForPlaceholders: Array<TextBox>
get() = try { get() = try {
Stats.onNativeCall() Stats.onNativeCall()
interopScope { interopScope {
...@@ -142,7 +143,7 @@ class Paragraph internal constructor(ptr: NativePointer, text: ManagedString?) : ...@@ -142,7 +143,7 @@ class Paragraph internal constructor(ptr: NativePointer, text: ManagedString?) :
fun getWordBoundary(offset: Int): IRange { fun getWordBoundary(offset: Int): IRange {
return try { return try {
Stats.onNativeCall() Stats.onNativeCall()
toIRange(_nGetWordBoundary(_ptr, offset)) IRange.fromInteropPointer { _nGetWordBoundary(_ptr, offset, it) }
} finally { } finally {
reachabilityBarrier(this) reachabilityBarrier(this)
} }
...@@ -305,7 +306,7 @@ private external fun _nGetRectsForRange( ...@@ -305,7 +306,7 @@ private external fun _nGetRectsForRange(
end: Int, end: Int,
rectHeightMode: Int, rectHeightMode: Int,
rectWidthMode: Int rectWidthMode: Int
): Array<TextBox> ): InteropPointer
@ExternalSymbolName("org_jetbrains_skia_paragraph_Paragraph__1nGetRectsForPlaceholders") @ExternalSymbolName("org_jetbrains_skia_paragraph_Paragraph__1nGetRectsForPlaceholders")
...@@ -315,7 +316,7 @@ private external fun _nGetRectsForPlaceholders(ptr: NativePointer): InteropPoint ...@@ -315,7 +316,7 @@ private external fun _nGetRectsForPlaceholders(ptr: NativePointer): InteropPoint
private external fun _nGetGlyphPositionAtCoordinate(ptr: NativePointer, dx: Float, dy: Float): Int private external fun _nGetGlyphPositionAtCoordinate(ptr: NativePointer, dx: Float, dy: Float): Int
@ExternalSymbolName("org_jetbrains_skia_paragraph_Paragraph__1nGetWordBoundary") @ExternalSymbolName("org_jetbrains_skia_paragraph_Paragraph__1nGetWordBoundary")
private external fun _nGetWordBoundary(ptr: NativePointer, offset: Int): Long private external fun _nGetWordBoundary(ptr: NativePointer, offset: Int, result: InteropPointer)
@ExternalSymbolName("org_jetbrains_skia_paragraph_Paragraph__1nGetLineMetrics") @ExternalSymbolName("org_jetbrains_skia_paragraph_Paragraph__1nGetLineMetrics")
private external fun _nGetLineMetrics(ptr: NativePointer, textPtr: NativePointer): InteropPointer private external fun _nGetLineMetrics(ptr: NativePointer, textPtr: NativePointer): InteropPointer
......
package org.jetbrains.skia.paragraph package org.jetbrains.skia.paragraph
import org.jetbrains.skia.* import org.jetbrains.skia.ExternalSymbolName
import org.jetbrains.skia.impl.* import org.jetbrains.skia.Rect
import org.jetbrains.skia.impl.ArrayInteropDecoder
import org.jetbrains.skia.impl.InteropPointer
import org.jetbrains.skia.impl.interopScope
class TextBox(val rect: Rect, direction: Direction) { class TextBox(val rect: Rect, direction: Direction) {
val _direction: Direction val _direction: Direction
...@@ -42,7 +45,11 @@ class TextBox(val rect: Rect, direction: Direction) { ...@@ -42,7 +45,11 @@ class TextBox(val rect: Rect, direction: Direction) {
val rect = FloatArray(4) val rect = FloatArray(4)
val direction = IntArray(1) val direction = IntArray(1)
interopScope { interopScope {
TextBox_nGetArrayElement(array, index, toInterop(rect), toInterop(direction)) val rectPtr = toInterop(rect)
val directionPtr = toInterop(direction)
TextBox_nGetArrayElement(array, index, rectPtr, directionPtr)
rectPtr.fromInterop(rect)
directionPtr.fromInterop(direction)
} }
return TextBox(rect[0], rect[1], rect[2], rect[3], direction[0]) return TextBox(rect[0], rect[1], rect[2], rect[3], direction[0])
} }
......
package org.jetbrains.skia package org.jetbrains.skia
import org.jetbrains.skia.impl.use import org.jetbrains.skia.impl.use
import org.jetbrains.skia.paragraph.FontCollection import org.jetbrains.skia.paragraph.*
import org.jetbrains.skia.paragraph.LineMetrics import org.jetbrains.skia.tests.assertCloseEnough
import org.jetbrains.skia.paragraph.ParagraphBuilder import org.jetbrains.skia.tests.assertContentCloseEnough
import org.jetbrains.skia.paragraph.ParagraphStyle import org.jetbrains.skia.tests.makeFromResource
import org.jetbrains.skiko.tests.SkipJsTarget import org.jetbrains.skiko.tests.SkipJsTarget
import org.jetbrains.skiko.tests.SkipNativeTarget import org.jetbrains.skiko.tests.SkipNativeTarget
import org.jetbrains.skiko.tests.runTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
@SkipJsTarget
@SkipNativeTarget
class ParagraphTest { class ParagraphTest {
private val fontCollection = FontCollection().setDefaultFontManager(FontMgr.default) private val fontCollection = suspend {
FontCollection().setDefaultFontManager(TypefaceFontProvider().apply {
registerTypeface(Typeface.makeFromResource("./fonts/Inter-Hinted-Regular.ttf"), "Inter")
})
}
private val style = ParagraphStyle().apply {
height = 40.0f
maxLinesCount = 2
textStyle = TextStyle().apply {
fontFamilies = arrayOf("Inter")
fontSize = 14.0f
}
}
@Test @Test
fun findTypefaces() { @SkipJsTarget
fontCollection.findTypefaces(emptyArray(), FontStyle.NORMAL) @SkipNativeTarget
fun findTypefaces() = runTest {
fontCollection().findTypefaces(emptyArray(), FontStyle.NORMAL)
} }
private fun singleLineMetrics(text: String): LineMetrics { private suspend fun singleLineMetrics(text: String): LineMetrics {
val style = ParagraphStyle() return ParagraphBuilder(style, fontCollection()).use {
return ParagraphBuilder(style, fontCollection).use {
it.addText(text) it.addText(text)
it.build() it.build()
}.layout(Float.POSITIVE_INFINITY).lineMetrics.first() }.layout(Float.POSITIVE_INFINITY).lineMetrics.first()
} }
@Test @Test
fun layoutParagraph() { @SkipJsTarget
@SkipNativeTarget
fun layoutParagraph() = runTest {
singleLineMetrics("aa").let { lineMetrics -> // latin singleLineMetrics("aa").let { lineMetrics -> // latin
assertEquals(0, lineMetrics.startIndex) assertEquals(0, lineMetrics.startIndex)
assertEquals(2, lineMetrics.endIndex) assertEquals(2, lineMetrics.endIndex)
...@@ -44,4 +59,50 @@ class ParagraphTest { ...@@ -44,4 +59,50 @@ class ParagraphTest {
assertEquals(2, lineMetrics.endExcludingWhitespaces) assertEquals(2, lineMetrics.endExcludingWhitespaces)
} }
} }
@Test
fun canCreate() = runTest {
val text = "Hello,\n Пользователь1!"
var paragraph = ParagraphBuilder(style, fontCollection()).use {
it.addText(text)
it.build()
}.layout(100.0f)
assertCloseEnough(100.0f, paragraph.maxWidth,0.01f)
assertCloseEnough(102.63f, paragraph.minIntrinsicWidth,0.01f)
assertCloseEnough(110.47f, paragraph.maxIntrinsicWidth,0.01f)
assertCloseEnough(13.5625f, paragraph.alphabeticBaseline, 0.01f)
assertCloseEnough(16.943085f, paragraph.ideographicBaseline, 0.01f)
assertCloseEnough(92.3125f, paragraph.longestLine,0.01f)
assertCloseEnough(34.0f, paragraph.height)
assertTrue(paragraph.didExceedMaxLines())
assertEquals(IRange(0, 5), paragraph.getWordBoundary(0))
assertEquals(IRange(8, 21), paragraph.getWordBoundary(10))
assertEquals(0, paragraph.unresolvedGlyphsCount)
assertEquals(2, paragraph.lineNumber)
assertContentEquals(arrayOf(), paragraph.rectsForPlaceholders)
assertContentCloseEnough(arrayOf(
TextBox(Rect(3.94f, 17.06f, 92.31f, 34.0f), Direction.LTR)
), paragraph.getRectsForRange(8, 21, RectHeightMode.TIGHT, RectWidthMode.TIGHT),0.01f)
paragraph = paragraph
.updateFontSize(8, 21, 48.0f)
.updateForegroundPaint(8, 21, Paint().apply { color = Color.RED })
.updateBackgroundPaint(8, 21, Paint().apply { color = Color.BLACK })
.updateAlignment(Alignment.RIGHT)
.markDirty()
assertContentCloseEnough(arrayOf(
TextBox(Rect(3.94f, 17.06f, 92.31f, 34.0f), Direction.LTR)
), paragraph.getRectsForRange(8, 21, RectHeightMode.TIGHT, RectWidthMode.TIGHT),0.01f)
assertEquals(
PositionWithAffinity(5, Affinity.UPSTREAM),
paragraph.getGlyphPositionAtCoordinate(30f, 10f)
)
}
} }
\ No newline at end of file
...@@ -4,6 +4,7 @@ import org.jetbrains.skia.Color4f ...@@ -4,6 +4,7 @@ import org.jetbrains.skia.Color4f
import org.jetbrains.skia.Matrix33 import org.jetbrains.skia.Matrix33
import org.jetbrains.skia.Point import org.jetbrains.skia.Point
import org.jetbrains.skia.Rect import org.jetbrains.skia.Rect
import org.jetbrains.skia.paragraph.TextBox
import org.jetbrains.skiko.KotlinBackend import org.jetbrains.skiko.KotlinBackend
import org.jetbrains.skiko.kotlinBackend import org.jetbrains.skiko.kotlinBackend
import kotlin.math.abs import kotlin.math.abs
...@@ -25,6 +26,10 @@ private inline fun Rect.isCloseEnoughTo(rect: Rect, epsilon: Float): Boolean = ...@@ -25,6 +26,10 @@ private inline fun Rect.isCloseEnoughTo(rect: Rect, epsilon: Float): Boolean =
left.isCloseEnoughTo(rect.left, epsilon) && right.isCloseEnoughTo(rect.right, epsilon) left.isCloseEnoughTo(rect.left, epsilon) && right.isCloseEnoughTo(rect.right, epsilon)
&& top.isCloseEnoughTo(rect.top, epsilon) && bottom.isCloseEnoughTo(rect.bottom, epsilon) && top.isCloseEnoughTo(rect.top, epsilon) && bottom.isCloseEnoughTo(rect.bottom, epsilon)
private inline fun TextBox.isCloseEnoughTo(textBox: TextBox, epsilon: Float = EPSILON): Boolean {
return (direction == textBox.direction) && rect.isCloseEnoughTo(textBox.rect, epsilon)
}
internal fun assertCloseEnough(expected: Float, actual: Float, epsilon: Float = EPSILON) { internal fun assertCloseEnough(expected: Float, actual: Float, epsilon: Float = EPSILON) {
assertTrue(expected.isCloseEnoughTo(actual, epsilon), message = "expected=$expected, actual=$actual, eps=$epsilon") assertTrue(expected.isCloseEnoughTo(actual, epsilon), message = "expected=$expected, actual=$actual, eps=$epsilon")
} }
...@@ -33,6 +38,10 @@ internal fun assertCloseEnough(expected: Point, actual: Point, epsilon: Float = ...@@ -33,6 +38,10 @@ internal fun assertCloseEnough(expected: Point, actual: Point, epsilon: Float =
assertTrue(expected.isCloseEnoughTo(actual, epsilon), message = "expected=$expected, actual=$actual, eps=$epsilon") assertTrue(expected.isCloseEnoughTo(actual, epsilon), message = "expected=$expected, actual=$actual, eps=$epsilon")
} }
internal fun assertCloseEnough(expected: TextBox, actual: TextBox, epsilon: Float = EPSILON) {
assertTrue(expected.isCloseEnoughTo(actual, epsilon), message = "expected=$expected, actual=$actual, eps=$epsilon")
}
internal fun assertCloseEnough(expected: Matrix33, actual: Matrix33, epsilon: Float = EPSILON) { internal fun assertCloseEnough(expected: Matrix33, actual: Matrix33, epsilon: Float = EPSILON) {
assertTrue( assertTrue(
expected.mat.zip(actual.mat).all { (a, b) -> a.isCloseEnoughTo(b, epsilon) }, expected.mat.zip(actual.mat).all { (a, b) -> a.isCloseEnoughTo(b, epsilon) },
...@@ -80,3 +89,7 @@ internal fun assertContentCloseEnough(expected: FloatArray, actual: FloatArray, ...@@ -80,3 +89,7 @@ internal fun assertContentCloseEnough(expected: FloatArray, actual: FloatArray,
internal fun assertContentCloseEnough(expected: Array<Point>, actual: Array<Point>, epsilon: Float = EPSILON) { internal fun assertContentCloseEnough(expected: Array<Point>, actual: Array<Point>, epsilon: Float = EPSILON) {
assertContentEquivalent(expected.iterator(), actual.iterator()) { a, b -> a.isCloseEnoughTo(b, epsilon) } assertContentEquivalent(expected.iterator(), actual.iterator()) { a, b -> a.isCloseEnoughTo(b, epsilon) }
} }
internal fun assertContentCloseEnough(expected: Array<TextBox>, actual: Array<TextBox>, epsilon: Float = EPSILON) {
assertContentEquivalent(expected.iterator(), actual.iterator()) { a, b -> a.isCloseEnoughTo(b, epsilon) }
}
...@@ -117,11 +117,15 @@ extern "C" JNIEXPORT jint JNICALL Java_org_jetbrains_skia_paragraph_ParagraphKt_ ...@@ -117,11 +117,15 @@ extern "C" JNIEXPORT jint JNICALL Java_org_jetbrains_skia_paragraph_ParagraphKt_
return -p.position-1; return -p.position-1;
} }
extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_paragraph_ParagraphKt__1nGetWordBoundary extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_paragraph_ParagraphKt__1nGetWordBoundary
(JNIEnv* env, jclass jclass, jlong ptr, jint offset) { (JNIEnv* env, jclass jclass, jlong ptr, jint offset, jintArray resultArray) {
Paragraph* instance = reinterpret_cast<Paragraph*>(static_cast<uintptr_t>(ptr)); Paragraph* instance = reinterpret_cast<Paragraph*>(static_cast<uintptr_t>(ptr));
SkRange<size_t> range = instance->getWordBoundary(offset); SkRange<size_t> range = instance->getWordBoundary(offset);
return packTwoInts(range.start & 0xFFFFFFFF, range.end & 0xFFFFFFFF); jint result[] {
static_cast<jint>(range.start & 0xFFFFFFFF),
static_cast<jint>(range.end & 0xFFFFFFFF)
};
env->SetIntArrayRegion(resultArray, 0, 2, result);
} }
extern "C" JNIEXPORT jobjectArray JNICALL Java_org_jetbrains_skia_paragraph_ParagraphKt__1nGetLineMetrics extern "C" JNIEXPORT jobjectArray JNICALL Java_org_jetbrains_skia_paragraph_ParagraphKt__1nGetLineMetrics
......
...@@ -78,27 +78,12 @@ SKIKO_EXPORT void org_jetbrains_skia_paragraph_Paragraph__1nPaint ...@@ -78,27 +78,12 @@ SKIKO_EXPORT void org_jetbrains_skia_paragraph_Paragraph__1nPaint
instance->paint(canvas, x, y); instance->paint(canvas, x, y);
} }
SKIKO_EXPORT KInteropPointerArray org_jetbrains_skia_paragraph_Paragraph__1nGetRectsForRange
(KNativePointer ptr, KInt start, KInt end, KInt rectHeightStyle, KInt rectWidthStyle) {
TODO("implement org_jetbrains_skia_paragraph_Paragraph__1nGetRectsForRange");
}
#if 0
SKIKO_EXPORT KInteropPointerArray org_jetbrains_skia_paragraph_Paragraph__1nGetRectsForRange SKIKO_EXPORT KInteropPointerArray org_jetbrains_skia_paragraph_Paragraph__1nGetRectsForRange
(KNativePointer ptr, KInt start, KInt end, KInt rectHeightStyle, KInt rectWidthStyle) { (KNativePointer ptr, KInt start, KInt end, KInt rectHeightStyle, KInt rectWidthStyle) {
Paragraph* instance = reinterpret_cast<Paragraph*>((ptr)); Paragraph* instance = reinterpret_cast<Paragraph*>((ptr));
std::vector<TextBox> rects = instance->getRectsForRange(start, end, static_cast<RectHeightStyle>(rectHeightStyle), static_cast<RectWidthStyle>(rectWidthStyle)); std::vector<TextBox> *rects = new std::vector<TextBox>(instance->getRectsForRange(start, end, static_cast<RectHeightStyle>(rectHeightStyle), static_cast<RectWidthStyle>(rectWidthStyle)));
KInteropPointerArray rectsArray = env->NewObjectArray((jsize) rects.size(), skija::paragraph::TextBox::cls, nullptr); return rects;
for (int i = 0; i < rects.size(); ++i) {
TextBox box = rects[i];
KInteropPointer boxObj = env->NewObject(skija::paragraph::TextBox::cls, skija::paragraph::TextBox::ctor, box.rect.fLeft, box.rect.fTop, box.rect.fRight, box.rect.fBottom, static_cast<KInt>(box.direction));
env->SetObjectArrayElement(rectsArray, i, boxObj);
env->DeleteLocalRef(boxObj);
}
return rectsArray;
} }
#endif
SKIKO_EXPORT KInteropPointerArray org_jetbrains_skia_paragraph_Paragraph__1nGetRectsForPlaceholders SKIKO_EXPORT KInteropPointerArray org_jetbrains_skia_paragraph_Paragraph__1nGetRectsForPlaceholders
(KNativePointer ptr) { (KNativePointer ptr) {
...@@ -117,11 +102,13 @@ SKIKO_EXPORT KInt org_jetbrains_skia_paragraph_Paragraph__1nGetGlyphPositionAtCo ...@@ -117,11 +102,13 @@ SKIKO_EXPORT KInt org_jetbrains_skia_paragraph_Paragraph__1nGetGlyphPositionAtCo
return -p.position-1; return -p.position-1;
} }
SKIKO_EXPORT KLong org_jetbrains_skia_paragraph_Paragraph__1nGetWordBoundary SKIKO_EXPORT void org_jetbrains_skia_paragraph_Paragraph__1nGetWordBoundary
(KNativePointer ptr, KInt offset) { (KNativePointer ptr, KInt offset, KInteropPointer resultPtr) {
Paragraph* instance = reinterpret_cast<Paragraph*>((ptr)); Paragraph* instance = reinterpret_cast<Paragraph*>((ptr));
SkRange<size_t> range = instance->getWordBoundary(offset); SkRange<size_t> range = instance->getWordBoundary(offset);
return packTwoInts(range.start & 0xFFFFFFFF, range.end & 0xFFFFFFFF); KInt* result = reinterpret_cast<KInt*>(resultPtr);
result[0] = range.start & 0xFFFFFFFF;
result[1] = range.end & 0xFFFFFFFF;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment