From 5375f4b5fd6b243591ebcc71257b92e3c867b168 Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Wed, 12 Feb 2025 18:01:23 +0100 Subject: [PATCH] implement android.text.Layout using PangoLayout This is needed to have proper line wrapping in composeUI apps. In the CTS, android.text.cts.StaticLayoutTest passes all checks now. --- meson.build | 1 + .../generated_headers/android_text_Layout.h | 149 ++++++++++++ src/api-impl-jni/text/android_text_Layout.c | 151 ++++++++++++ src/api-impl/android/graphics/Paint.java | 2 +- src/api-impl/android/text/Editable.java | 2 + src/api-impl/android/text/Layout.java | 226 ++++++++++++++++-- src/api-impl/android/text/StaticLayout.java | 16 +- 7 files changed, 519 insertions(+), 28 deletions(-) create mode 100644 src/api-impl-jni/generated_headers/android_text_Layout.h create mode 100644 src/api-impl-jni/text/android_text_Layout.c diff --git a/meson.build b/meson.build index 2f152a77..4976aa31 100644 --- a/meson.build +++ b/meson.build @@ -111,6 +111,7 @@ libtranslationlayer_so = shared_library('translation_layer_main', [ 'src/api-impl-jni/net/android_net_ConnectivityManager.c', 'src/api-impl-jni/os/android_os_Process.c', 'src/api-impl-jni/sensors/android_hardware_SensorManager.c', + 'src/api-impl-jni/text/android_text_Layout.c', 'src/api-impl-jni/util.c', 'src/api-impl-jni/views/AndroidLayout.c', 'src/api-impl-jni/views/android_view_View.c', diff --git a/src/api-impl-jni/generated_headers/android_text_Layout.h b/src/api-impl-jni/generated_headers/android_text_Layout.h new file mode 100644 index 00000000..40dbf67a --- /dev/null +++ b/src/api-impl-jni/generated_headers/android_text_Layout.h @@ -0,0 +1,149 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class android_text_Layout */ + +#ifndef _Included_android_text_Layout +#define _Included_android_text_Layout +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: android_text_Layout + * Method: native_constructor + * Signature: (Ljava/lang/String;JI)J + */ +JNIEXPORT jlong JNICALL Java_android_text_Layout_native_1constructor + (JNIEnv *, jobject, jstring, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_set_width + * Signature: (JI)V + */ +JNIEXPORT void JNICALL Java_android_text_Layout_native_1set_1width + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_get_width + * Signature: (J)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1width + (JNIEnv *, jobject, jlong); + +/* + * Class: android_text_Layout + * Method: native_get_height + * Signature: (J)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1height + (JNIEnv *, jobject, jlong); + +/* + * Class: android_text_Layout + * Method: native_get_line_count + * Signature: (J)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1count + (JNIEnv *, jobject, jlong); + +/* + * Class: android_text_Layout + * Method: native_get_line_top + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1top + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_get_line_bottom + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1bottom + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_get_line_left + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1left + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_get_line_right + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1right + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_get_line_width + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1width + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_get_line_baseline + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1baseline + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_get_line_ascent + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1ascent + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_get_line_descent + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1descent + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_get_line_for_vertical + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1for_1vertical + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_set_ellipsize + * Signature: (JIF)V + */ +JNIEXPORT void JNICALL Java_android_text_Layout_native_1set_1ellipsize + (JNIEnv *, jobject, jlong, jint, jfloat); + +/* + * Class: android_text_Layout + * Method: native_get_ellipsis_count + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1ellipsis_1count + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: android_text_Layout + * Method: native_draw + * Signature: (JJJ)V + */ +JNIEXPORT void JNICALL Java_android_text_Layout_native_1draw + (JNIEnv *, jobject, jlong, jlong, jlong); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/api-impl-jni/text/android_text_Layout.c b/src/api-impl-jni/text/android_text_Layout.c new file mode 100644 index 00000000..ce9270e5 --- /dev/null +++ b/src/api-impl-jni/text/android_text_Layout.c @@ -0,0 +1,151 @@ +#include +#include + +#include "../defines.h" +#include "../graphics/AndroidPaint.h" +#include "../generated_headers/android_text_Layout.h" + +extern GtkWidget *window; + +JNIEXPORT jlong JNICALL Java_android_text_Layout_native_1constructor(JNIEnv *env, jobject object, jstring text, jlong paint, jint width) +{ + struct AndroidPaint *android_paint = _PTR(paint); + PangoLayout *layout = pango_layout_new(gtk_widget_get_pango_context(window)); + pango_layout_set_font_description(layout, android_paint->font); + const char *str = (*env)->GetStringUTFChars(env, text, NULL); + pango_layout_set_text(layout, str, -1); + (*env)->ReleaseStringUTFChars(env, text, str); + pango_layout_set_width(layout, width * PANGO_SCALE); + return _INTPTR(layout); +} + +JNIEXPORT void JNICALL Java_android_text_Layout_native_1set_1width(JNIEnv *env, jobject object, jlong layout, jint width) +{ + PangoLayout *pango_layout = _PTR(layout); + pango_layout_set_width(pango_layout, width * PANGO_SCALE); +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1width(JNIEnv *env, jobject object, jlong layout) +{ + PangoLayout *pango_layout = _PTR(layout); + return pango_layout_get_width(pango_layout) / PANGO_SCALE; +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1height(JNIEnv *env, jobject object, jlong layout) +{ + PangoLayout *pango_layout = _PTR(layout); + PangoRectangle ink_rect; + PangoRectangle logical_rect; + pango_layout_get_extents(pango_layout, &ink_rect, &logical_rect); + return logical_rect.height / PANGO_SCALE; +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1count(JNIEnv *env, jobject object, jlong layout) +{ + PangoLayout *pango_layout = _PTR(layout); + return pango_layout_get_line_count(pango_layout); +} + +static void get_line_extends(PangoLayout *pango_layout, int line, PangoRectangle *logical_rect) { + PangoRectangle ink_rect; + PangoLayoutIter *pango_iter = pango_layout_get_iter(pango_layout); + while (line--) + pango_layout_iter_next_line(pango_iter); + pango_layout_iter_get_line_extents(pango_iter, &ink_rect, logical_rect); + pango_layout_iter_free(pango_iter); +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1top(JNIEnv *env, jobject object, jlong layout, jint line) +{ + PangoRectangle logical_rect; + get_line_extends(PANGO_LAYOUT(_PTR(layout)), line, &logical_rect); + return (logical_rect.y) / PANGO_SCALE; +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1bottom(JNIEnv *env, jobject object, jlong layout, jint line) +{ + PangoRectangle logical_rect; + get_line_extends(PANGO_LAYOUT(_PTR(layout)), line, &logical_rect); + return (logical_rect.y + logical_rect.height) / PANGO_SCALE; +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1left(JNIEnv *env, jobject object, jlong layout, jint line) +{ + PangoRectangle logical_rect; + get_line_extends(PANGO_LAYOUT(_PTR(layout)), line, &logical_rect); + return logical_rect.x / PANGO_SCALE; +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1right(JNIEnv *env, jobject object, jlong layout, jint line) +{ + PangoRectangle logical_rect; + get_line_extends(PANGO_LAYOUT(_PTR(layout)), line, &logical_rect); + return (logical_rect.x + logical_rect.width) / PANGO_SCALE; +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1width(JNIEnv *env, jobject object, jlong layout, jint line) +{ + PangoRectangle logical_rect; + get_line_extends(PANGO_LAYOUT(_PTR(layout)), line, &logical_rect); + return logical_rect.width / PANGO_SCALE; +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1baseline(JNIEnv *env, jobject object, jlong layout, jint line) +{ + PangoLayout *pango_layout = _PTR(layout); + PangoLayoutIter *pango_iter = pango_layout_get_iter(pango_layout); + while (line--) + pango_layout_iter_next_line(pango_iter); + + return pango_layout_iter_get_baseline(pango_iter) / PANGO_SCALE; +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1ascent(JNIEnv *env, jobject object, jlong layout, jint line) +{ + PangoLayout *pango_layout = _PTR(layout); + PangoLayoutLine *pango_line = pango_layout_get_line_readonly(pango_layout, line); + PangoRectangle logical_rect, ink_rect; + pango_layout_line_get_extents(pango_line, &logical_rect, &ink_rect); + return -PANGO_ASCENT(ink_rect) / PANGO_SCALE; +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1descent(JNIEnv *env, jobject object, jlong layout, jint line) +{ + PangoLayout *pango_layout = _PTR(layout); + PangoLayoutLine *pango_line = pango_layout_get_line_readonly(pango_layout, line); + PangoRectangle logical_rect, ink_rect; + pango_layout_line_get_extents(pango_line, &logical_rect, &ink_rect); + return PANGO_DESCENT(ink_rect) / PANGO_SCALE; +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1line_1for_1vertical(JNIEnv *env, jobject object, jlong layout, jint y) +{ + PangoLayout *pango_layout = _PTR(layout); + int index_, trailing; + pango_layout_xy_to_index(pango_layout, 0, y * PANGO_SCALE, &index_, &trailing); + int line, x_pos; + pango_layout_index_to_line_x(pango_layout, index_, trailing, &line, &x_pos); + return line; +} + +JNIEXPORT void JNICALL Java_android_text_Layout_native_1set_1ellipsize(JNIEnv *env, jobject object, jlong layout, jint ellipsize_mode, jfloat ellipsize_width) +{ + PangoLayout *pango_layout = _PTR(layout); + pango_layout_set_ellipsize(pango_layout, (PangoEllipsizeMode)ellipsize_mode); + pango_layout_set_width(pango_layout, ellipsize_width * PANGO_SCALE); +} + +JNIEXPORT jint JNICALL Java_android_text_Layout_native_1get_1ellipsis_1count(JNIEnv *env, jobject object, jlong layout, jint line) +{ + PangoLayout *pango_layout = _PTR(layout); + return pango_layout_is_ellipsized(pango_layout); +} + +JNIEXPORT void JNICALL Java_android_text_Layout_native_1draw(JNIEnv *env, jobject object, jlong layout, jlong snapshot_ptr, jlong paint_ptr) +{ + PangoLayout *pango_layout = PANGO_LAYOUT(_PTR(layout)); + GtkSnapshot *snapshot = GTK_SNAPSHOT(_PTR(snapshot_ptr)); + struct AndroidPaint *android_paint = _PTR(paint_ptr); + + gtk_snapshot_append_layout(snapshot, pango_layout, &android_paint->color); +} diff --git a/src/api-impl/android/graphics/Paint.java b/src/api-impl/android/graphics/Paint.java index 4132b73b..05457ee5 100644 --- a/src/api-impl/android/graphics/Paint.java +++ b/src/api-impl/android/graphics/Paint.java @@ -17,7 +17,7 @@ public class Paint { public static final int AUTO_HINTING_TEXT_FLAG = (1 << 11); public static final int VERTICAL_TEXT_FLAG = (1 << 12); - long paint; // native paint + public long paint; // native paint private Xfermode xfermode; private Shader shader; diff --git a/src/api-impl/android/text/Editable.java b/src/api-impl/android/text/Editable.java index 78660463..059f0e5f 100644 --- a/src/api-impl/android/text/Editable.java +++ b/src/api-impl/android/text/Editable.java @@ -20,4 +20,6 @@ public interface Editable extends CharSequence { public void setFilters(InputFilter[] filters); + public Editable delete(int start, int end); + } diff --git a/src/api-impl/android/text/Layout.java b/src/api-impl/android/text/Layout.java index 0666a74b..ddb0d179 100644 --- a/src/api-impl/android/text/Layout.java +++ b/src/api-impl/android/text/Layout.java @@ -1,6 +1,8 @@ package android.text; import android.graphics.Canvas; +import android.graphics.GskCanvas; +import android.graphics.Path; public class Layout { @@ -12,69 +14,241 @@ public class Layout { ALIGN_RIGHT, } + public class Directions {} + + long layout; // native PangoLayout private CharSequence text; private TextPaint paint; + private float spacing_mult; + private float spacing_add; + private Alignment align; protected Layout(CharSequence text, TextPaint paint, int width, Layout.Alignment align, float spacingMult, float spacingAdd) { this.text = text; this.paint = paint; + this.spacing_mult = spacingMult; + this.spacing_add = spacingAdd; + this.align = align; + layout = native_constructor(text.toString(), paint.paint, width); } - public int getLineCount() {return 1;} + public int getLineCount() { + return native_get_line_count(layout); + } - public float getLineWidth(int line) {return 10;} + public float getLineWidth(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_line_width(layout, line); + } - public TextPaint getPaint() {return new TextPaint();} + public TextPaint getPaint() { + return paint; + } - public int getEllipsisCount(int line) {return 0;} + public int getEllipsisCount(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_ellipsis_count(layout, line); + } public CharSequence getText() {return text;} - public int getWidth() {return 10;} + public int getWidth() { + return native_get_width(layout); + } public int getHeight() { - return (int)(paint.measureText("_") * 3); + System.out.println("height = " + native_get_height(layout)); + System.out.println("should be " + ((int)(paint.measureText("_") * 3))); + return native_get_height(layout); } public void draw(Canvas canvas) { - canvas.drawText(text.toString(), 0, 0, paint); + if (canvas instanceof GskCanvas) + native_draw(layout, ((GskCanvas)canvas).snapshot, paint.paint); + else + canvas.drawText(text.toString(), 0, 0, paint); } - public int getParagraphDirection(int line) {return 0;} + public int getParagraphDirection(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return 0; + } public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint) { return paint.measureText(source, start, end); } - public int getLineEnd(int line) {return 100;} + public int getLineBaseline(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_line_baseline(layout, line); + } - public int getLineStart(int line) {return 0;} + public int getLineAscent(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_line_ascent(layout, line); + } - public int getLineAscent(int line) {return 0;} + public int getLineDescent(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_line_descent(layout, line); + } - public int getLineDescent(int line) {return 0;} + public int getTopPadding() {return -5;} - public int getTopPadding() {return 0;} - - public int getBottomPadding() {return 0;} - - public float getLineLeft(int line) {return 0;} - - public int getLineBottom(int line) {return 0;} - - public int getLineBaseline(int line) {return 0;} + public int getBottomPadding() {return 5;} public boolean isRtlCharAt(int offset) {return false;} - public float getSecondaryHorizontal(int line) {return 0;} + public float getSecondaryHorizontal(int line) { + if (getLineDirections(0) == null) + throw new NullPointerException(); + return 0; + } - public int getLineForVertical(int y) {return 0;} + public int getLineForVertical(int y) { + return native_get_line_for_vertical(layout, y); + } - public int getOffsetForHorizontal(int line, float x) {return 0;} + public int getOffsetForHorizontal(int line, float x) { + if (getLineDirections(0) == null) + throw new NullPointerException(); + return 0; + } - public float getPrimaryHorizontal(int line) {return 0;} + public float getPrimaryHorizontal(int line) { + if (getLineDirections(0) == null) + throw new NullPointerException(); + return 0; + } public int getLineForOffset(int offset) {return 0;} - public int getLineTop(int line) {return 0;} + public int getLineTop(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_line_top(layout, line); + } + + public int getLineBottom(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_line_bottom(layout, line); + } + + public float getLineLeft(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_line_left(layout, line); + } + + public float getLineRight(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_line_right(layout, line); + } + + public int getLineStart(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_line_left(layout, line); + } + + public int getLineEnd(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return native_get_line_right(layout, line); + } + + public boolean isSpanned() { + return text instanceof Spanned; + } + + public void increaseWidthTo(int width) { + if (width < getWidth()) + throw new RuntimeException("cannot decrease width"); + native_set_width(layout, width); + } + + public float getSpacingMultiplier() { + return spacing_mult; + } + + public float getSpacing_add() { + return spacing_add; + } + + public void getSelectionPath(int start, int end, Path path) { + if (start != end && getLineDirections(0) == null) + throw new NullPointerException(); + } + + public int getParagraphRight(int line) { + return getWidth(); + } + + public int getParagraphLeft(int line) { + return 0; + } + + public Alignment getParagraphAlignment(int line) { + return align; + } + + public int getOffsetToRightOf(int offset) { + if (getLineDirections(0) == null) + throw new NullPointerException(); + return offset; + } + + public int getOffsetToLeftOf(int offset) { + if (getLineDirections(0) == null) + throw new NullPointerException(); + return offset; + } + + public Directions getLineDirections(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return new Directions(); + } + + public int getLineVisibleEnd(int line) { + return getLineEnd(line); + } + + public boolean getLineContainsTab(int line) { return text.toString().split("\n")[line].contains("\t"); } + + public int getEllipsizedWidth() { + return getWidth(); + } + + public int getEllipsisStart(int line) { + if (line < 0 || line >= getLineCount()) + throw new ArrayIndexOutOfBoundsException(); + return 0; + } + + protected native long native_constructor(String text, long paint, int width); + protected native void native_set_width(long layout, int width); + protected native int native_get_width(long layout); + protected native int native_get_height(long layout); + protected native int native_get_line_count(long layout); + protected native int native_get_line_top(long layout, int line); + protected native int native_get_line_bottom(long layout, int line); + protected native int native_get_line_left(long layout, int line); + protected native int native_get_line_right(long layout, int line); + protected native int native_get_line_width(long layout, int line); + protected native int native_get_line_baseline(long layout, int line); + protected native int native_get_line_ascent(long layout, int line); + protected native int native_get_line_descent(long layout, int line); + protected native int native_get_line_for_vertical(long layout, int y); + protected native void native_set_ellipsize(long layout, int ellipsize_mode, float ellipsize_width); + protected native int native_get_ellipsis_count(long layout, int line); + protected native void native_draw(long layout, long snapshot, long paint); } diff --git a/src/api-impl/android/text/StaticLayout.java b/src/api-impl/android/text/StaticLayout.java index d99b02ca..2a5c669c 100644 --- a/src/api-impl/android/text/StaticLayout.java +++ b/src/api-impl/android/text/StaticLayout.java @@ -8,7 +8,21 @@ public class StaticLayout extends Layout { float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) { - super(source, paint, outerwidth, align, spacingmult, spacingadd); + super(source.toString(), paint, outerwidth, align, spacingmult, spacingadd); + } + + public StaticLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad) { + super(source.toString(), paint, outerwidth, align, spacingmult, spacingadd); + } + + public StaticLayout(CharSequence source, int start, int end, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + super(source.toString(), paint, outerwidth, align, spacingmult, spacingadd); + if (ellipsize != null) + native_set_ellipsize(layout, ellipsize == null ? 0 : ellipsize.ordinal()+1, ellipsizedWidth); + } + + public StaticLayout(CharSequence source, int start, int end, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad) { + super(source.toString(), paint, outerwidth, align, spacingmult, spacingadd); } public static class Builder {