diff --git a/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsPublication.kt b/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsPublication.kt index d3ef3d611ceee..f68ad0d36f933 100644 --- a/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsPublication.kt +++ b/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsPublication.kt @@ -53,6 +53,7 @@ object JetBrainsPublication { supportedPlatforms = ComposePlatforms.SKIKO_SUPPORT, ), ComposeComponent(":compose:ui:ui-graphics"), + ComposeComponent(":compose:ui:ui-skiko"), ComposeComponent(":compose:ui:ui-test"), ComposeComponent( ":compose:ui:ui-test-junit4", diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle index fa56a7554af11..58e119f97f3d6 100644 --- a/compose/foundation/foundation/build.gradle +++ b/compose/foundation/foundation/build.gradle @@ -124,6 +124,9 @@ androidXMultiplatform { dependencies { // TODO(https://youtrack.jetbrains.com/issue/CMP-219) Remove API api(libs.skiko) + // TODO(https://youtrack.jetbrains.com/issue/CMP-10338): Remove direct dependency + implementation(project(":compose:ui:ui-skiko")) + implementation(libs.atomicFu) } } diff --git a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/OnClickTest.kt b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/OnClickTest.kt index 73e711bb646be..d88e312a346c5 100644 --- a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/OnClickTest.kt +++ b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/OnClickTest.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.FrameRecomposer import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.ViewConfiguration +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.scene.CanvasLayersComposeScene import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.runSkikoComposeUiTest @@ -40,6 +41,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.use +import kotlin.test.BeforeTest import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -48,6 +50,19 @@ import kotlinx.coroutines.test.runTest @OptIn(ExperimentalFoundationApi::class, InternalComposeUiApi::class) class OnClickTest { + @BeforeTest + fun registerSkikoBackend() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once tests that register the backend asynchronously + // (e.g. AWT ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on + // the registration persisting across tests. + // @AfterTest + // fun clearSkikoBackend() { + // clearSkikoComposeImplementation() + // } + private fun testClick( pointerMatcher: PointerMatcher, button: PointerButton diff --git a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/copyPasteAndroidTests/text/TextDelegateIntegrationTest.kt b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/copyPasteAndroidTests/text/TextDelegateIntegrationTest.kt index d6642c346b3af..6400a9b4ef52d 100644 --- a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/copyPasteAndroidTests/text/TextDelegateIntegrationTest.kt +++ b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/copyPasteAndroidTests/text/TextDelegateIntegrationTest.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.isEqualTo import androidx.compose.foundation.isTrue import androidx.compose.foundation.text.InternalFoundationTextApi import androidx.compose.foundation.text.TextDelegate +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle @@ -32,12 +34,26 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.sp import kotlin.math.roundToInt +import kotlin.test.BeforeTest import kotlin.test.Ignore import kotlin.test.Test -@OptIn(InternalFoundationTextApi::class) +@OptIn(InternalFoundationTextApi::class, InternalComposeUiApi::class) class TextDelegateIntegrationTest { + @BeforeTest + fun registerSkikoBackend() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once tests that register the backend asynchronously + // (e.g. AWT ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on + // the registration persisting across tests. + // @AfterTest + // fun clearSkikoBackend() { + // clearSkikoComposeImplementation() + // } + @Test @Ignore // TODO: test is failing fun minIntrinsicWidth_getter() = with(Density(1f, 1f)) { diff --git a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/AbsoluteCutCornerShapeTest.kt b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/AbsoluteCutCornerShapeTest.kt index 67a6fc4e9d9c9..47a47b97c00e5 100644 --- a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/AbsoluteCutCornerShapeTest.kt +++ b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/AbsoluteCutCornerShapeTest.kt @@ -16,6 +16,9 @@ package androidx.compose.foundation.shape +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import kotlin.test.BeforeTest import androidx.compose.foundation.assertThat import androidx.compose.foundation.isEqualTo import androidx.compose.foundation.isFalse @@ -31,8 +34,22 @@ import androidx.compose.ui.unit.dp import kotlin.test.Test import kotlin.test.assertEquals +@OptIn(InternalComposeUiApi::class) class AbsoluteCutCornerShapeTest { + @BeforeTest + fun registerSkikoBackend() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once tests that register the backend asynchronously + // (e.g. AWT ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on + // the registration persisting across tests. + // @AfterTest + // fun clearSkikoBackend() { + // clearSkikoComposeImplementation() + // } + private var layoutDirection: LayoutDirection? = null private val testParameters = arrayOf( diff --git a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/AbsoluteRoundedCornerShapeTest.kt b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/AbsoluteRoundedCornerShapeTest.kt index 5a21e49c8aa86..735179cf5a50b 100644 --- a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/AbsoluteRoundedCornerShapeTest.kt +++ b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/AbsoluteRoundedCornerShapeTest.kt @@ -16,6 +16,9 @@ package androidx.compose.foundation.shape +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import kotlin.test.BeforeTest import androidx.compose.foundation.assertThat import androidx.compose.foundation.isEqualTo import androidx.compose.foundation.isFalse @@ -32,8 +35,22 @@ import androidx.compose.ui.unit.dp import kotlin.test.Test import kotlin.test.assertEquals +@OptIn(InternalComposeUiApi::class) class AbsoluteRoundedCornerShapeTest { + @BeforeTest + fun registerSkikoBackend() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once tests that register the backend asynchronously + // (e.g. AWT ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on + // the registration persisting across tests. + // @AfterTest + // fun clearSkikoBackend() { + // clearSkikoComposeImplementation() + // } + private var layoutDirection: LayoutDirection? = null private val testParameters = arrayOf( diff --git a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/CutCornerShapeTest.kt b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/CutCornerShapeTest.kt index 89e213cad3f6e..5deb9d5f45fe7 100644 --- a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/CutCornerShapeTest.kt +++ b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/CutCornerShapeTest.kt @@ -16,6 +16,9 @@ package androidx.compose.foundation.shape +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import kotlin.test.BeforeTest import androidx.compose.foundation.assertThat import androidx.compose.foundation.isEqualTo import androidx.compose.foundation.isFalse @@ -33,8 +36,22 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue +@OptIn(InternalComposeUiApi::class) class CutCornerShapeTest { + @BeforeTest + fun registerSkikoBackend() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once tests that register the backend asynchronously + // (e.g. AWT ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on + // the registration persisting across tests. + // @AfterTest + // fun clearSkikoBackend() { + // clearSkikoComposeImplementation() + // } + private val density = Density(2f) private val size = Size(100.0f, 150.0f) diff --git a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/RoundedCornerShapeTest.kt b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/RoundedCornerShapeTest.kt index 4ad2e985246b8..d4fe78e85bd5a 100644 --- a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/RoundedCornerShapeTest.kt +++ b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/shape/RoundedCornerShapeTest.kt @@ -16,6 +16,9 @@ package androidx.compose.foundation.shape +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import kotlin.test.BeforeTest import androidx.compose.foundation.assertThat import androidx.compose.foundation.isEqualTo import androidx.compose.foundation.isFalse @@ -32,8 +35,22 @@ import androidx.compose.ui.unit.dp import kotlin.test.Test import kotlin.test.assertEquals +@OptIn(InternalComposeUiApi::class) class RoundedCornerShapeTest { + @BeforeTest + fun registerSkikoBackend() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once tests that register the backend asynchronously + // (e.g. AWT ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on + // the registration persisting across tests. + // @AfterTest + // fun clearSkikoBackend() { + // clearSkikoComposeImplementation() + // } + private val density = Density(2f) private val size = Size(100.0f, 150.0f) diff --git a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/CupertinoTextFieldDelegateTest.kt b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/CupertinoTextFieldDelegateTest.kt index ec22bc9af2f4b..c9727975c0d67 100644 --- a/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/CupertinoTextFieldDelegateTest.kt +++ b/compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/CupertinoTextFieldDelegateTest.kt @@ -16,6 +16,9 @@ package androidx.compose.foundation.text +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import kotlin.test.BeforeTest import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.MultiParagraph import androidx.compose.ui.text.TextLayoutInput @@ -34,7 +37,21 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlinx.test.IgnoreWasmTarget +@OptIn(InternalComposeUiApi::class) class CupertinoTextFieldDelegateTest { + + @BeforeTest + fun registerSkikoBackend() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once tests that register the backend asynchronously + // (e.g. AWT ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on + // the registration persisting across tests. + // @AfterTest + // fun clearSkikoBackend() { + // clearSkikoComposeImplementation() + // } private val sampleText = "aaaa bbb cccc dd e fffffffff?????????!!!!!!! ...\n" + "ggggggg tttt\n" + "Family emoji: \uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66, and some text at the end\n" + diff --git a/compose/mpp/demo/build.gradle.kts b/compose/mpp/demo/build.gradle.kts index 6d4dcf7c190b2..9f51f1c498001 100644 --- a/compose/mpp/demo/build.gradle.kts +++ b/compose/mpp/demo/build.gradle.kts @@ -136,6 +136,7 @@ kotlin { implementation(project(":compose:ui:ui-graphics")) implementation(project(":compose:ui:ui-text")) implementation(project(":compose:ui:ui-backhandler")) + implementation(project(":compose:ui:ui-skiko")) implementation(project(":lifecycle:lifecycle-common")) implementation(project(":lifecycle:lifecycle-runtime")) implementation(project(":lifecycle:lifecycle-runtime-compose")) diff --git a/compose/ui/ui-graphics/api/desktop/ui-graphics.api b/compose/ui/ui-graphics/api/desktop/ui-graphics.api index 157a4200c1a81..12c843d683842 100644 --- a/compose/ui/ui-graphics/api/desktop/ui-graphics.api +++ b/compose/ui/ui-graphics/api/desktop/ui-graphics.api @@ -315,38 +315,6 @@ public final class androidx/compose/ui/graphics/DegreesKt { public static final fun degrees (F)F } -public final class androidx/compose/ui/graphics/DesktopColorFilter_desktopKt { - public static final synthetic fun asDesktopColorFilter (Landroidx/compose/ui/graphics/ColorFilter;)Lorg/jetbrains/skia/ColorFilter; - public static final synthetic fun toComposeColorFilter (Lorg/jetbrains/skia/ColorFilter;)Landroidx/compose/ui/graphics/ColorFilter; -} - -public final class androidx/compose/ui/graphics/DesktopImageAsset_desktopKt { - public static final synthetic fun asDesktopBitmap (Landroidx/compose/ui/graphics/ImageBitmap;)Lorg/jetbrains/skia/Bitmap; - public static final synthetic fun asImageBitmap (Lorg/jetbrains/skia/Bitmap;)Landroidx/compose/ui/graphics/ImageBitmap; - public static final synthetic fun asImageBitmap (Lorg/jetbrains/skia/Image;)Landroidx/compose/ui/graphics/ImageBitmap; -} - -public final class androidx/compose/ui/graphics/DesktopImageConverters_desktopKt { - public static final synthetic fun asAwtImage (Landroidx/compose/ui/graphics/ImageBitmap;)Ljava/awt/image/BufferedImage; - public static final synthetic fun asAwtImage-Ug5Nnss (Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;J)Ljava/awt/Image; - public static synthetic fun asAwtImage-Ug5Nnss$default (Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;JILjava/lang/Object;)Ljava/awt/Image; - public static final synthetic fun asPainter (Ljava/awt/image/BufferedImage;)Landroidx/compose/ui/graphics/painter/Painter; - public static final fun toAwtImage (Landroidx/compose/ui/graphics/ImageBitmap;)Ljava/awt/image/BufferedImage; - public static final fun toAwtImage-Ug5Nnss (Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;J)Ljava/awt/Image; - public static synthetic fun toAwtImage-Ug5Nnss$default (Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;JILjava/lang/Object;)Ljava/awt/Image; - public static final fun toComposeBitmap (Ljava/awt/image/BufferedImage;)Landroidx/compose/ui/graphics/ImageBitmap; - public static final fun toComposeImageBitmap (Ljava/awt/image/BufferedImage;)Landroidx/compose/ui/graphics/ImageBitmap; - public static final fun toPainter (Ljava/awt/image/BufferedImage;)Landroidx/compose/ui/graphics/painter/Painter; -} - -public final class androidx/compose/ui/graphics/DesktopPathEffect_desktopKt { - public static final synthetic fun asDesktopPathEffect (Landroidx/compose/ui/graphics/PathEffect;)Lorg/jetbrains/skia/PathEffect; -} - -public final class androidx/compose/ui/graphics/DesktopPath_desktopKt { - public static final synthetic fun asDesktopPath (Landroidx/compose/ui/graphics/Path;)Lorg/jetbrains/skia/Path; -} - public abstract interface annotation class androidx/compose/ui/graphics/ExperimentalGraphicsApi : java/lang/annotation/Annotation { } @@ -645,6 +613,11 @@ public final class androidx/compose/ui/graphics/PaintKt { public static final field DefaultAlpha F } +public final class androidx/compose/ui/graphics/Paint_nonAndroidKt { + public static final fun Paint ()Landroidx/compose/ui/graphics/Paint; + public static final fun isSupported-s9anfk8 (I)Z +} + public final class androidx/compose/ui/graphics/PaintingStyle { public static final field Companion Landroidx/compose/ui/graphics/PaintingStyle$Companion; public static final synthetic fun box-impl (I)Landroidx/compose/ui/graphics/PaintingStyle; @@ -814,6 +787,11 @@ public final class androidx/compose/ui/graphics/PathIterator$ConicEvaluation : j public static fun values ()[Landroidx/compose/ui/graphics/PathIterator$ConicEvaluation; } +public final class androidx/compose/ui/graphics/PathIterator_nonAndroidKt { + public static final fun PathIterator (Landroidx/compose/ui/graphics/Path;Landroidx/compose/ui/graphics/PathIterator$ConicEvaluation;F)Landroidx/compose/ui/graphics/PathIterator; + public static synthetic fun PathIterator$default (Landroidx/compose/ui/graphics/Path;Landroidx/compose/ui/graphics/PathIterator$ConicEvaluation;FILjava/lang/Object;)Landroidx/compose/ui/graphics/PathIterator; +} + public final class androidx/compose/ui/graphics/PathKt { public static final fun copy (Landroidx/compose/ui/graphics/Path;)Landroidx/compose/ui/graphics/Path; } @@ -831,6 +809,10 @@ public final class androidx/compose/ui/graphics/PathMeasure$DefaultImpls { public static synthetic fun getSegment$default (Landroidx/compose/ui/graphics/PathMeasure;FFLandroidx/compose/ui/graphics/Path;ZILjava/lang/Object;)Z } +public final class androidx/compose/ui/graphics/PathMeasure_nonAndroidKt { + public static final fun PathMeasure ()Landroidx/compose/ui/graphics/PathMeasure; +} + public final class androidx/compose/ui/graphics/PathOperation { public static final field Companion Landroidx/compose/ui/graphics/PathOperation$Companion; public static final synthetic fun box-impl (I)Landroidx/compose/ui/graphics/PathOperation; @@ -894,6 +876,10 @@ public final class androidx/compose/ui/graphics/PathSvgKt { public static synthetic fun toSvg$default (Landroidx/compose/ui/graphics/Path;ZILjava/lang/Object;)Ljava/lang/String; } +public final class androidx/compose/ui/graphics/Path_nonAndroidKt { + public static final fun Path ()Landroidx/compose/ui/graphics/Path; +} + public final class androidx/compose/ui/graphics/PixelMap { public static final field $stable I public fun ([IIIII)V @@ -939,16 +925,9 @@ public final class androidx/compose/ui/graphics/RectangleShapeKt { public static final fun getRectangleShape ()Landroidx/compose/ui/graphics/Shape; } -public final class androidx/compose/ui/graphics/Rects_skikoKt { - public static final fun toComposeRect (Lorg/jetbrains/skia/Rect;)Landroidx/compose/ui/geometry/Rect; - public static final fun toSkiaRRect (Landroidx/compose/ui/geometry/RoundRect;)Lorg/jetbrains/skia/RRect; - public static final fun toSkiaRect (Landroidx/compose/ui/geometry/Rect;)Lorg/jetbrains/skia/Rect; -} - public abstract class androidx/compose/ui/graphics/RenderEffect { public static final field $stable I public final fun asSkiaImageFilter ()Lorg/jetbrains/skia/ImageFilter; - protected abstract fun createImageFilter ()Lorg/jetbrains/skia/ImageFilter; public fun isSupported ()Z } @@ -1014,66 +993,6 @@ public abstract interface class androidx/compose/ui/graphics/Shape { public abstract fun createOutline-Pq9zytI (JLandroidx/compose/ui/unit/LayoutDirection;Landroidx/compose/ui/unit/Density;)Landroidx/compose/ui/graphics/Outline; } -public final class androidx/compose/ui/graphics/SkiaBackedCanvas_skikoKt { - public static final fun asComposeCanvas (Lorg/jetbrains/skia/Canvas;)Landroidx/compose/ui/graphics/Canvas; - public static final fun getNativeCanvas (Landroidx/compose/ui/graphics/Canvas;)Lorg/jetbrains/skia/Canvas; - public static final fun getSkiaCanvas (Landroidx/compose/ui/graphics/Canvas;)Lorg/jetbrains/skia/Canvas; -} - -public final class androidx/compose/ui/graphics/SkiaBackedPaint_skikoKt { - public static final fun Paint ()Landroidx/compose/ui/graphics/Paint; - public static final fun asComposePaint (Lorg/jetbrains/skia/Paint;)Landroidx/compose/ui/graphics/Paint; - public static final fun getSkiaPaint (Landroidx/compose/ui/graphics/Paint;)Lorg/jetbrains/skia/Paint; - public static final fun isSupported-s9anfk8 (I)Z -} - -public final class androidx/compose/ui/graphics/SkiaBackedPathEffect_skikoKt { - public static final fun asComposePathEffect (Lorg/jetbrains/skia/PathEffect;)Landroidx/compose/ui/graphics/PathEffect; - public static final fun asSkiaPathEffect (Landroidx/compose/ui/graphics/PathEffect;)Lorg/jetbrains/skia/PathEffect; -} - -public final class androidx/compose/ui/graphics/SkiaBackedPathMeasure_skikoKt { - public static final fun PathMeasure ()Landroidx/compose/ui/graphics/PathMeasure; - public static final fun asComposePathEffect (Lorg/jetbrains/skia/PathMeasure;)Landroidx/compose/ui/graphics/PathMeasure; - public static final fun asSkiaPathMeasure (Landroidx/compose/ui/graphics/PathMeasure;)Lorg/jetbrains/skia/PathMeasure; -} - -public final class androidx/compose/ui/graphics/SkiaBackedPath_skikoKt { - public static final fun Path ()Landroidx/compose/ui/graphics/Path; - public static final fun asComposePath (Lorg/jetbrains/skia/Path;)Landroidx/compose/ui/graphics/Path; - public static final fun asSkiaPath (Landroidx/compose/ui/graphics/Path;)Lorg/jetbrains/skia/Path; -} - -public final class androidx/compose/ui/graphics/SkiaBackedRenderEffect_skikoKt { - public static final fun asComposeRenderEffect (Lorg/jetbrains/skia/ImageFilter;)Landroidx/compose/ui/graphics/RenderEffect; - public static final fun getSkiaImageFilter (Landroidx/compose/ui/graphics/RenderEffect;)Lorg/jetbrains/skia/ImageFilter; -} - -public final class androidx/compose/ui/graphics/SkiaColorFilter_skikoKt { - public static final fun asComposeColorFilter (Lorg/jetbrains/skia/ColorFilter;)Landroidx/compose/ui/graphics/ColorFilter; - public static final fun asSkiaColorFilter (Landroidx/compose/ui/graphics/ColorFilter;)Lorg/jetbrains/skia/ColorFilter; -} - -public final class androidx/compose/ui/graphics/SkiaImageAsset_skikoKt { - public static final fun asComposeImageBitmap (Lorg/jetbrains/skia/Bitmap;)Landroidx/compose/ui/graphics/ImageBitmap; - public static final fun asSkiaBitmap (Landroidx/compose/ui/graphics/ImageBitmap;)Lorg/jetbrains/skia/Bitmap; - public static final fun toComposeImageBitmap (Lorg/jetbrains/skia/Image;)Landroidx/compose/ui/graphics/ImageBitmap; -} - -public final class androidx/compose/ui/graphics/SkiaPathIterator_skikoKt { - public static final fun PathIterator (Landroidx/compose/ui/graphics/Path;Landroidx/compose/ui/graphics/PathIterator$ConicEvaluation;F)Landroidx/compose/ui/graphics/PathIterator; - public static synthetic fun PathIterator$default (Landroidx/compose/ui/graphics/Path;Landroidx/compose/ui/graphics/PathIterator$ConicEvaluation;FILjava/lang/Object;)Landroidx/compose/ui/graphics/PathIterator; -} - -public final class androidx/compose/ui/graphics/SkiaShader_skikoKt { - public static final fun asComposeShader (Lorg/jetbrains/skia/Shader;)Landroidx/compose/ui/graphics/Shader; - public static final fun getSkiaShader (Landroidx/compose/ui/graphics/Shader;)Lorg/jetbrains/skia/Shader; -} - -public final class androidx/compose/ui/graphics/SkiaTileMode_skikoKt { - public static final fun isSupported-0vamqd0 (I)Z -} - public final class androidx/compose/ui/graphics/SolidColor : androidx/compose/ui/graphics/Brush, androidx/compose/ui/graphics/Interpolatable { public static final field $stable I public synthetic fun (JLkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -1172,6 +1091,10 @@ public final class androidx/compose/ui/graphics/TileMode$Companion { public final fun getRepeated-3opZhB0 ()I } +public final class androidx/compose/ui/graphics/TileMode_nonAndroidKt { + public static final fun isSupported-0vamqd0 (I)Z +} + public final class androidx/compose/ui/graphics/VertexMode { public static final field Companion Landroidx/compose/ui/graphics/VertexMode$Companion; public static final synthetic fun box-impl (I)Landroidx/compose/ui/graphics/VertexMode; diff --git a/compose/ui/ui-graphics/api/ui-graphics.klib.api b/compose/ui/ui-graphics/api/ui-graphics.klib.api index cb67eefdf59a3..5137c8358f717 100644 --- a/compose/ui/ui-graphics/api/ui-graphics.klib.api +++ b/compose/ui/ui-graphics/api/ui-graphics.klib.api @@ -1883,7 +1883,6 @@ sealed class androidx.compose.ui.graphics/Outline { // androidx.compose.ui.graph } sealed class androidx.compose.ui.graphics/RenderEffect { // androidx.compose.ui.graphics/RenderEffect|null[0] - abstract fun createImageFilter(): org.jetbrains.skia/ImageFilter // androidx.compose.ui.graphics/RenderEffect.createImageFilter|createImageFilter(){}[0] final fun asSkiaImageFilter(): org.jetbrains.skia/ImageFilter // androidx.compose.ui.graphics/RenderEffect.asSkiaImageFilter|asSkiaImageFilter(){}[0] open fun isSupported(): kotlin/Boolean // androidx.compose.ui.graphics/RenderEffect.isSupported|isSupported(){}[0] } @@ -1978,6 +1977,8 @@ final val androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_pain final val androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_BrushPainter$stableprop // androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_BrushPainter$stableprop|#static{}androidx_compose_ui_graphics_painter_BrushPainter$stableprop[0] final val androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_ColorPainter$stableprop // androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_ColorPainter$stableprop|#static{}androidx_compose_ui_graphics_painter_ColorPainter$stableprop[0] final val androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_Painter$stableprop // androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_Painter$stableprop|#static{}androidx_compose_ui_graphics_painter_Painter$stableprop[0] +final val androidx.compose.ui.graphics.platform/androidx_compose_ui_graphics_platform_PlatformGraphicsContext$stableprop // androidx.compose.ui.graphics.platform/androidx_compose_ui_graphics_platform_PlatformGraphicsContext$stableprop|#static{}androidx_compose_ui_graphics_platform_PlatformGraphicsContext$stableprop[0] +final val androidx.compose.ui.graphics.platform/androidx_compose_ui_graphics_platform_PlatformGraphicsRegistry$stableprop // androidx.compose.ui.graphics.platform/androidx_compose_ui_graphics_platform_PlatformGraphicsRegistry$stableprop|#static{}androidx_compose_ui_graphics_platform_PlatformGraphicsRegistry$stableprop[0] final val androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_DropShadowPainter$stableprop // androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_DropShadowPainter$stableprop|#static{}androidx_compose_ui_graphics_shadow_DropShadowPainter$stableprop[0] final val androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_InnerShadowPainter$stableprop // androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_InnerShadowPainter$stableprop|#static{}androidx_compose_ui_graphics_shadow_InnerShadowPainter$stableprop[0] final val androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_Shadow$stableprop // androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_Shadow$stableprop|#static{}androidx_compose_ui_graphics_shadow_Shadow$stableprop[0] @@ -2032,7 +2033,7 @@ final val androidx.compose.ui.graphics/androidx_compose_ui_graphics_RenderEffect final val androidx.compose.ui.graphics/androidx_compose_ui_graphics_Shader$stableprop // androidx.compose.ui.graphics/androidx_compose_ui_graphics_Shader$stableprop|#static{}androidx_compose_ui_graphics_Shader$stableprop[0] final val androidx.compose.ui.graphics/androidx_compose_ui_graphics_ShaderBrush$stableprop // androidx.compose.ui.graphics/androidx_compose_ui_graphics_ShaderBrush$stableprop|#static{}androidx_compose_ui_graphics_ShaderBrush$stableprop[0] final val androidx.compose.ui.graphics/androidx_compose_ui_graphics_Shadow$stableprop // androidx.compose.ui.graphics/androidx_compose_ui_graphics_Shadow$stableprop|#static{}androidx_compose_ui_graphics_Shadow$stableprop[0] -final val androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop // androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop|#static{}androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop[0] +final val androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkikoGraphicsCompatRegistry$stableprop // androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkikoGraphicsCompatRegistry$stableprop|#static{}androidx_compose_ui_graphics_SkikoGraphicsCompatRegistry$stableprop[0] final val androidx.compose.ui.graphics/androidx_compose_ui_graphics_SolidColor$stableprop // androidx.compose.ui.graphics/androidx_compose_ui_graphics_SolidColor$stableprop|#static{}androidx_compose_ui_graphics_SolidColor$stableprop[0] final val androidx.compose.ui.graphics/androidx_compose_ui_graphics_SweepGradient$stableprop // androidx.compose.ui.graphics/androidx_compose_ui_graphics_SweepGradient$stableprop|#static{}androidx_compose_ui_graphics_SweepGradient$stableprop[0] final val androidx.compose.ui.graphics/androidx_compose_ui_graphics_Vertices$stableprop // androidx.compose.ui.graphics/androidx_compose_ui_graphics_Vertices$stableprop|#static{}androidx_compose_ui_graphics_Vertices$stableprop[0] @@ -2044,25 +2045,13 @@ final val androidx.compose.ui.graphics/isSpecified // androidx.compose.ui.graphi final inline fun (androidx.compose.ui.graphics/Color).(): kotlin/Boolean // androidx.compose.ui.graphics/isSpecified.|@androidx.compose.ui.graphics.Color(){}[0] final val androidx.compose.ui.graphics/isUnspecified // androidx.compose.ui.graphics/isUnspecified|@androidx.compose.ui.graphics.Color{}isUnspecified[0] final inline fun (androidx.compose.ui.graphics/Color).(): kotlin/Boolean // androidx.compose.ui.graphics/isUnspecified.|@androidx.compose.ui.graphics.Color(){}[0] -final val androidx.compose.ui.graphics/nativeCanvas // androidx.compose.ui.graphics/nativeCanvas|@androidx.compose.ui.graphics.Canvas{}nativeCanvas[0] - final fun (androidx.compose.ui.graphics/Canvas).(): org.jetbrains.skia/Canvas // androidx.compose.ui.graphics/nativeCanvas.|@androidx.compose.ui.graphics.Canvas(){}[0] final val androidx.compose.ui.graphics/reverseDifference // androidx.compose.ui.graphics/reverseDifference|@androidx.compose.ui.graphics.PathOperation.Companion{}reverseDifference[0] final fun (androidx.compose.ui.graphics/PathOperation.Companion).(): androidx.compose.ui.graphics/PathOperation // androidx.compose.ui.graphics/reverseDifference.|@androidx.compose.ui.graphics.PathOperation.Companion(){}[0] -final val androidx.compose.ui.graphics/skiaCanvas // androidx.compose.ui.graphics/skiaCanvas|@androidx.compose.ui.graphics.Canvas{}skiaCanvas[0] - final fun (androidx.compose.ui.graphics/Canvas).(): org.jetbrains.skia/Canvas // androidx.compose.ui.graphics/skiaCanvas.|@androidx.compose.ui.graphics.Canvas(){}[0] -final val androidx.compose.ui.graphics/skiaImageFilter // androidx.compose.ui.graphics/skiaImageFilter|@androidx.compose.ui.graphics.RenderEffect{}skiaImageFilter[0] - final fun (androidx.compose.ui.graphics/RenderEffect).(): org.jetbrains.skia/ImageFilter // androidx.compose.ui.graphics/skiaImageFilter.|@androidx.compose.ui.graphics.RenderEffect(){}[0] -final val androidx.compose.ui.graphics/skiaPaint // androidx.compose.ui.graphics/skiaPaint|@androidx.compose.ui.graphics.Paint{}skiaPaint[0] - final fun (androidx.compose.ui.graphics/Paint).(): org.jetbrains.skia/Paint // androidx.compose.ui.graphics/skiaPaint.|@androidx.compose.ui.graphics.Paint(){}[0] -final val androidx.compose.ui.graphics/skiaShader // androidx.compose.ui.graphics/skiaShader|@androidx.compose.ui.graphics.Shader{}skiaShader[0] - final fun (androidx.compose.ui.graphics/Shader).(): org.jetbrains.skia/Shader // androidx.compose.ui.graphics/skiaShader.|@androidx.compose.ui.graphics.Shader(){}[0] final val androidx.compose.ui.graphics/union // androidx.compose.ui.graphics/union|@androidx.compose.ui.graphics.PathOperation.Companion{}union[0] final fun (androidx.compose.ui.graphics/PathOperation.Companion).(): androidx.compose.ui.graphics/PathOperation // androidx.compose.ui.graphics/union.|@androidx.compose.ui.graphics.PathOperation.Companion(){}[0] final val androidx.compose.ui.graphics/xor // androidx.compose.ui.graphics/xor|@androidx.compose.ui.graphics.PathOperation.Companion{}xor[0] final fun (androidx.compose.ui.graphics/PathOperation.Companion).(): androidx.compose.ui.graphics/PathOperation // androidx.compose.ui.graphics/xor.|@androidx.compose.ui.graphics.PathOperation.Companion(){}[0] -final fun (androidx.compose.ui.geometry/Rect).androidx.compose.ui.graphics/toSkiaRect(): org.jetbrains.skia/Rect // androidx.compose.ui.graphics/toSkiaRect|toSkiaRect@androidx.compose.ui.geometry.Rect(){}[0] -final fun (androidx.compose.ui.geometry/RoundRect).androidx.compose.ui.graphics/toSkiaRRect(): org.jetbrains.skia/RRect // androidx.compose.ui.graphics/toSkiaRRect|toSkiaRRect@androidx.compose.ui.geometry.RoundRect(){}[0] final fun (androidx.compose.ui.graphics.colorspace/ColorSpace).androidx.compose.ui.graphics.colorspace/adapt(androidx.compose.ui.graphics.colorspace/WhitePoint, androidx.compose.ui.graphics.colorspace/Adaptation = ...): androidx.compose.ui.graphics.colorspace/ColorSpace // androidx.compose.ui.graphics.colorspace/adapt|adapt@androidx.compose.ui.graphics.colorspace.ColorSpace(androidx.compose.ui.graphics.colorspace.WhitePoint;androidx.compose.ui.graphics.colorspace.Adaptation){}[0] final fun (androidx.compose.ui.graphics.colorspace/ColorSpace).androidx.compose.ui.graphics.colorspace/connect(androidx.compose.ui.graphics.colorspace/ColorSpace = ..., androidx.compose.ui.graphics.colorspace/RenderIntent = ...): androidx.compose.ui.graphics.colorspace/Connector // androidx.compose.ui.graphics.colorspace/connect|connect@androidx.compose.ui.graphics.colorspace.ColorSpace(androidx.compose.ui.graphics.colorspace.ColorSpace;androidx.compose.ui.graphics.colorspace.RenderIntent){}[0] final fun (androidx.compose.ui.graphics.drawscope/DrawScope).androidx.compose.ui.graphics.layer/drawLayer(androidx.compose.ui.graphics.layer/GraphicsLayer) // androidx.compose.ui.graphics.layer/drawLayer|drawLayer@androidx.compose.ui.graphics.drawscope.DrawScope(androidx.compose.ui.graphics.layer.GraphicsLayer){}[0] @@ -2077,34 +2066,18 @@ final fun (androidx.compose.ui.graphics/Canvas).androidx.compose.ui.graphics/sca final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/compositeOver(androidx.compose.ui.graphics/Color): androidx.compose.ui.graphics/Color // androidx.compose.ui.graphics/compositeOver|compositeOver@androidx.compose.ui.graphics.Color(androidx.compose.ui.graphics.Color){}[0] final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/luminance(): kotlin/Float // androidx.compose.ui.graphics/luminance|luminance@androidx.compose.ui.graphics.Color(){}[0] final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/toArgb(): kotlin/Int // androidx.compose.ui.graphics/toArgb|toArgb@androidx.compose.ui.graphics.Color(){}[0] -final fun (androidx.compose.ui.graphics/ColorFilter).androidx.compose.ui.graphics/asSkiaColorFilter(): org.jetbrains.skia/ColorFilter // androidx.compose.ui.graphics/asSkiaColorFilter|asSkiaColorFilter@androidx.compose.ui.graphics.ColorFilter(){}[0] -final fun (androidx.compose.ui.graphics/ImageBitmap).androidx.compose.ui.graphics/asSkiaBitmap(): org.jetbrains.skia/Bitmap // androidx.compose.ui.graphics/asSkiaBitmap|asSkiaBitmap@androidx.compose.ui.graphics.ImageBitmap(){}[0] final fun (androidx.compose.ui.graphics/ImageBitmap).androidx.compose.ui.graphics/toPixelMap(kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/IntArray = ..., kotlin/Int = ..., kotlin/Int = ...): androidx.compose.ui.graphics/PixelMap // androidx.compose.ui.graphics/toPixelMap|toPixelMap@androidx.compose.ui.graphics.ImageBitmap(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.IntArray;kotlin.Int;kotlin.Int){}[0] final fun (androidx.compose.ui.graphics/Matrix).androidx.compose.ui.graphics/isIdentity(): kotlin/Boolean // androidx.compose.ui.graphics/isIdentity|isIdentity@androidx.compose.ui.graphics.Matrix(){}[0] final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/addOutline(androidx.compose.ui.graphics/Outline) // androidx.compose.ui.graphics/addOutline|addOutline@androidx.compose.ui.graphics.Path(androidx.compose.ui.graphics.Outline){}[0] final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/addSvg(kotlin/String) // androidx.compose.ui.graphics/addSvg|addSvg@androidx.compose.ui.graphics.Path(kotlin.String){}[0] -final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/asSkiaPath(): org.jetbrains.skia/Path // androidx.compose.ui.graphics/asSkiaPath|asSkiaPath@androidx.compose.ui.graphics.Path(){}[0] final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/computeDirection(): androidx.compose.ui.graphics/Path.Direction // androidx.compose.ui.graphics/computeDirection|computeDirection@androidx.compose.ui.graphics.Path(){}[0] final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/copy(): androidx.compose.ui.graphics/Path // androidx.compose.ui.graphics/copy|copy@androidx.compose.ui.graphics.Path(){}[0] final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/divide(kotlin.collections/MutableList = ...): kotlin.collections/MutableList // androidx.compose.ui.graphics/divide|divide@androidx.compose.ui.graphics.Path(kotlin.collections.MutableList){}[0] final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/reverse(androidx.compose.ui.graphics/Path = ...): androidx.compose.ui.graphics/Path // androidx.compose.ui.graphics/reverse|reverse@androidx.compose.ui.graphics.Path(androidx.compose.ui.graphics.Path){}[0] final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/toSvg(kotlin/Boolean = ...): kotlin/String // androidx.compose.ui.graphics/toSvg|toSvg@androidx.compose.ui.graphics.Path(kotlin.Boolean){}[0] -final fun (androidx.compose.ui.graphics/PathEffect).androidx.compose.ui.graphics/asSkiaPathEffect(): org.jetbrains.skia/PathEffect // androidx.compose.ui.graphics/asSkiaPathEffect|asSkiaPathEffect@androidx.compose.ui.graphics.PathEffect(){}[0] -final fun (androidx.compose.ui.graphics/PathMeasure).androidx.compose.ui.graphics/asSkiaPathMeasure(): org.jetbrains.skia/PathMeasure // androidx.compose.ui.graphics/asSkiaPathMeasure|asSkiaPathMeasure@androidx.compose.ui.graphics.PathMeasure(){}[0] final fun (androidx.compose.ui.graphics/TileMode).androidx.compose.ui.graphics/isSupported(): kotlin/Boolean // androidx.compose.ui.graphics/isSupported|isSupported@androidx.compose.ui.graphics.TileMode(){}[0] final fun (kotlin.collections/List).androidx.compose.ui.graphics.vector/toPath(androidx.compose.ui.graphics/Path = ...): androidx.compose.ui.graphics/Path // androidx.compose.ui.graphics.vector/toPath|toPath@kotlin.collections.List(androidx.compose.ui.graphics.Path){}[0] final fun (kotlin/ByteArray).androidx.compose.ui.graphics/decodeToImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // androidx.compose.ui.graphics/decodeToImageBitmap|decodeToImageBitmap@kotlin.ByteArray(){}[0] -final fun (org.jetbrains.skia/Bitmap).androidx.compose.ui.graphics/asComposeImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // androidx.compose.ui.graphics/asComposeImageBitmap|asComposeImageBitmap@org.jetbrains.skia.Bitmap(){}[0] -final fun (org.jetbrains.skia/Canvas).androidx.compose.ui.graphics/asComposeCanvas(): androidx.compose.ui.graphics/Canvas // androidx.compose.ui.graphics/asComposeCanvas|asComposeCanvas@org.jetbrains.skia.Canvas(){}[0] -final fun (org.jetbrains.skia/ColorFilter).androidx.compose.ui.graphics/asComposeColorFilter(): androidx.compose.ui.graphics/ColorFilter // androidx.compose.ui.graphics/asComposeColorFilter|asComposeColorFilter@org.jetbrains.skia.ColorFilter(){}[0] -final fun (org.jetbrains.skia/Image).androidx.compose.ui.graphics/toComposeImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // androidx.compose.ui.graphics/toComposeImageBitmap|toComposeImageBitmap@org.jetbrains.skia.Image(){}[0] -final fun (org.jetbrains.skia/ImageFilter).androidx.compose.ui.graphics/asComposeRenderEffect(): androidx.compose.ui.graphics/RenderEffect // androidx.compose.ui.graphics/asComposeRenderEffect|asComposeRenderEffect@org.jetbrains.skia.ImageFilter(){}[0] -final fun (org.jetbrains.skia/Paint).androidx.compose.ui.graphics/asComposePaint(): androidx.compose.ui.graphics/Paint // androidx.compose.ui.graphics/asComposePaint|asComposePaint@org.jetbrains.skia.Paint(){}[0] -final fun (org.jetbrains.skia/Path).androidx.compose.ui.graphics/asComposePath(): androidx.compose.ui.graphics/Path // androidx.compose.ui.graphics/asComposePath|asComposePath@org.jetbrains.skia.Path(){}[0] -final fun (org.jetbrains.skia/PathEffect).androidx.compose.ui.graphics/asComposePathEffect(): androidx.compose.ui.graphics/PathEffect // androidx.compose.ui.graphics/asComposePathEffect|asComposePathEffect@org.jetbrains.skia.PathEffect(){}[0] -final fun (org.jetbrains.skia/PathMeasure).androidx.compose.ui.graphics/asComposePathEffect(): androidx.compose.ui.graphics/PathMeasure // androidx.compose.ui.graphics/asComposePathEffect|asComposePathEffect@org.jetbrains.skia.PathMeasure(){}[0] -final fun (org.jetbrains.skia/Rect).androidx.compose.ui.graphics/toComposeRect(): androidx.compose.ui.geometry/Rect // androidx.compose.ui.graphics/toComposeRect|toComposeRect@org.jetbrains.skia.Rect(){}[0] -final fun (org.jetbrains.skia/Shader).androidx.compose.ui.graphics/asComposeShader(): androidx.compose.ui.graphics/Shader // androidx.compose.ui.graphics/asComposeShader|asComposeShader@org.jetbrains.skia.Shader(){}[0] final fun androidx.compose.ui.graphics.colorspace/androidx_compose_ui_graphics_colorspace_Adaptation$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.colorspace/androidx_compose_ui_graphics_colorspace_Adaptation$stableprop_getter|androidx_compose_ui_graphics_colorspace_Adaptation$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics.colorspace/androidx_compose_ui_graphics_colorspace_ColorSpace$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.colorspace/androidx_compose_ui_graphics_colorspace_ColorSpace$stableprop_getter|androidx_compose_ui_graphics_colorspace_ColorSpace$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics.colorspace/androidx_compose_ui_graphics_colorspace_ColorSpaces$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.colorspace/androidx_compose_ui_graphics_colorspace_ColorSpaces$stableprop_getter|androidx_compose_ui_graphics_colorspace_ColorSpaces$stableprop_getter(){}[0] @@ -2123,6 +2096,8 @@ final fun androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_pain final fun androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_BrushPainter$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_BrushPainter$stableprop_getter|androidx_compose_ui_graphics_painter_BrushPainter$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_ColorPainter$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_ColorPainter$stableprop_getter|androidx_compose_ui_graphics_painter_ColorPainter$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_Painter$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.painter/androidx_compose_ui_graphics_painter_Painter$stableprop_getter|androidx_compose_ui_graphics_painter_Painter$stableprop_getter(){}[0] +final fun androidx.compose.ui.graphics.platform/androidx_compose_ui_graphics_platform_PlatformGraphicsContext$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.platform/androidx_compose_ui_graphics_platform_PlatformGraphicsContext$stableprop_getter|androidx_compose_ui_graphics_platform_PlatformGraphicsContext$stableprop_getter(){}[0] +final fun androidx.compose.ui.graphics.platform/androidx_compose_ui_graphics_platform_PlatformGraphicsRegistry$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.platform/androidx_compose_ui_graphics_platform_PlatformGraphicsRegistry$stableprop_getter|androidx_compose_ui_graphics_platform_PlatformGraphicsRegistry$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_DropShadowPainter$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_DropShadowPainter$stableprop_getter|androidx_compose_ui_graphics_shadow_DropShadowPainter$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_InnerShadowPainter$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_InnerShadowPainter$stableprop_getter|androidx_compose_ui_graphics_shadow_InnerShadowPainter$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_Shadow$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics.shadow/androidx_compose_ui_graphics_shadow_Shadow$stableprop_getter|androidx_compose_ui_graphics_shadow_Shadow$stableprop_getter(){}[0] @@ -2193,7 +2168,7 @@ final fun androidx.compose.ui.graphics/androidx_compose_ui_graphics_RenderEffect final fun androidx.compose.ui.graphics/androidx_compose_ui_graphics_Shader$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics/androidx_compose_ui_graphics_Shader$stableprop_getter|androidx_compose_ui_graphics_Shader$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics/androidx_compose_ui_graphics_ShaderBrush$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics/androidx_compose_ui_graphics_ShaderBrush$stableprop_getter|androidx_compose_ui_graphics_ShaderBrush$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics/androidx_compose_ui_graphics_Shadow$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics/androidx_compose_ui_graphics_Shadow$stableprop_getter|androidx_compose_ui_graphics_Shadow$stableprop_getter(){}[0] -final fun androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop_getter|androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop_getter(){}[0] +final fun androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkikoGraphicsCompatRegistry$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkikoGraphicsCompatRegistry$stableprop_getter|androidx_compose_ui_graphics_SkikoGraphicsCompatRegistry$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics/androidx_compose_ui_graphics_SolidColor$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics/androidx_compose_ui_graphics_SolidColor$stableprop_getter|androidx_compose_ui_graphics_SolidColor$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics/androidx_compose_ui_graphics_SweepGradient$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics/androidx_compose_ui_graphics_SweepGradient$stableprop_getter|androidx_compose_ui_graphics_SweepGradient$stableprop_getter(){}[0] final fun androidx.compose.ui.graphics/androidx_compose_ui_graphics_Vertices$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics/androidx_compose_ui_graphics_Vertices$stableprop_getter|androidx_compose_ui_graphics_Vertices$stableprop_getter(){}[0] diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle index 7c024438097e8..1aec3eeb9b0ad 100644 --- a/compose/ui/ui-graphics/build.gradle +++ b/compose/ui/ui-graphics/build.gradle @@ -95,38 +95,50 @@ androidXMultiplatform { implementation(project(":compose:test-utils")) } - // TODO: Align naming: nonAndroidMain - skikoMain { + nonAndroidMain { dependsOn(commonMain) dependencies { - api(libs.skiko) + // No skiko here: nonAndroidMain is skia-free. Skia lives in skikoMain (below). + implementation(libs.atomicFu) } } - skikoTest { + nonAndroidTest { dependsOn(commonTest) dependencies { implementation(project(":compose:ui:ui-test")) + implementation(project(":compose:ui:ui-skiko")) implementation(project(":kruth:kruth")) } } - skikoExcludingWebMain { + // Skia-bearing compatibility layer between the skia-free nonAndroidMain and the platform + // leaves. Holds public skia-typed API structurally pinned to ui-graphics (deprecated + // skia-returning members + Native* typealiases). + // commonMain -> nonAndroidMain -> skikoMain -> (desktop/native/web). + skikoMain { + dependsOn(nonAndroidMain) + dependencies { + api(libs.skiko) + } + } + + nonAndroidExcludingWebMain { dependsOn(skikoMain) } - skikoExcludingWebTest { - dependsOn(skikoTest) + nonAndroidExcludingWebTest { + dependsOn(nonAndroidTest) } desktopMain { dependsOn(skikoMain) - dependsOn(skikoExcludingWebMain) + dependsOn(nonAndroidExcludingWebMain) } desktopTest { - dependsOn(skikoTest) - dependsOn(skikoExcludingWebTest) + dependsOn(nonAndroidTest) + dependsOn(nonAndroidExcludingWebTest) resources.srcDirs += "src/desktopTest/res" dependencies { implementation(libs.junit) @@ -145,17 +157,17 @@ androidXMultiplatform { } nonJvmTest { - dependsOn(skikoTest) + dependsOn(nonAndroidTest) } nativeMain { dependsOn(nonJvmMain) - dependsOn(skikoExcludingWebMain) + dependsOn(nonAndroidExcludingWebMain) } nativeTest { dependsOn(nonJvmTest) - dependsOn(skikoExcludingWebTest) + dependsOn(nonAndroidExcludingWebTest) } wasmJsMain { @@ -191,4 +203,4 @@ tasks.withType(KotlinCompile).configureEach { task -> tasks.findByName("desktopTest").configure { systemProperties["GOLDEN_PATH"] = project.rootDir.absolutePath + "/golden" -} \ No newline at end of file +} diff --git a/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt b/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt index a3d49fea0c669..126170679f74f 100644 --- a/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt +++ b/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt @@ -42,7 +42,7 @@ abstract class DesktopGraphicsTest { protected fun initCanvas(widthPx: Int, heightPx: Int): Canvas { require(_surface == null) _surface = Surface.makeRasterN32Premul(widthPx, heightPx) - return SkiaBackedCanvas(_surface!!.canvas) + return _surface!!.canvas.asComposeCanvas() } @After diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Canvas.nonAndroid.kt similarity index 66% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt rename to compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Canvas.nonAndroid.kt index 55d4c50b67b22..fe72a69594da2 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Canvas.nonAndroid.kt @@ -16,11 +16,9 @@ package androidx.compose.ui.graphics -import org.jetbrains.skia.VertexMode as SkVertexMode +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry -internal fun VertexMode.toSkiaVertexMode(): SkVertexMode = when (this) { - VertexMode.Triangles -> SkVertexMode.TRIANGLES - VertexMode.TriangleStrip -> SkVertexMode.TRIANGLE_STRIP - VertexMode.TriangleFan -> SkVertexMode.TRIANGLE_FAN - else -> SkVertexMode.TRIANGLES -} +@OptIn(InternalComposeUiApi::class) +internal actual fun ActualCanvas(image: ImageBitmap): Canvas = + PlatformGraphicsRegistry.requireCurrent().createCanvas(image) diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/ColorFilter.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/ColorFilter.nonAndroid.kt new file mode 100644 index 0000000000000..53b808715d880 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/ColorFilter.nonAndroid.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformColorFilter +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry + +// Use `Any` to avoid double-wrapping and EXPECT_ACTUAL_INCOMPATIBLE_CLASS_KIND +internal actual typealias NativeColorFilter = Any // == PlatformColorFilter + +@OptIn(InternalComposeUiApi::class) +internal actual fun actualTintColorFilter(color: Color, blendMode: BlendMode): NativeColorFilter = + PlatformGraphicsRegistry.requireCurrent() + .createTintColorFilter(color, blendMode) + +@OptIn(InternalComposeUiApi::class) +internal actual fun actualColorMatrixColorFilter(colorMatrix: ColorMatrix): NativeColorFilter = + PlatformGraphicsRegistry.requireCurrent() + .createColorMatrixColorFilter(colorMatrix) + +@OptIn(InternalComposeUiApi::class) +internal actual fun actualLightingColorFilter(multiply: Color, add: Color): NativeColorFilter = + PlatformGraphicsRegistry.requireCurrent() + .createLightingColorFilter(multiply, add) + +@OptIn(InternalComposeUiApi::class) +internal actual fun actualColorMatrixFromFilter(filter: NativeColorFilter): ColorMatrix = + PlatformGraphicsRegistry.requireCurrent() + .colorMatrixFromFilter(filter as PlatformColorFilter) diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.nonAndroid.kt new file mode 100644 index 0000000000000..600e184cd2ce6 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.nonAndroid.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.colorspace.ColorSpace +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry + +@OptIn(InternalComposeUiApi::class) +internal actual fun ActualImageBitmap( + width: Int, + height: Int, + config: ImageBitmapConfig, + hasAlpha: Boolean, + colorSpace: ColorSpace, +): ImageBitmap = + PlatformGraphicsRegistry.requireCurrent().createImageBitmap( + width = width, + height = height, + config = config, + hasAlpha = hasAlpha, + colorSpace = colorSpace, + ) + +@OptIn(InternalComposeUiApi::class) +internal actual fun createImageBitmap(bytes: ByteArray): ImageBitmap = + PlatformGraphicsRegistry.requireCurrent().decodeImageBitmap(bytes) diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Paint.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Paint.nonAndroid.kt new file mode 100644 index 0000000000000..e398fec2256c5 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Paint.nonAndroid.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry + +@OptIn(InternalComposeUiApi::class) +actual fun Paint(): Paint = + PlatformGraphicsRegistry.requireCurrent().createPaint() + +@OptIn(InternalComposeUiApi::class) +actual fun BlendMode.isSupported(): Boolean = + PlatformGraphicsRegistry.requireCurrent().isBlendModeSupported(this) diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaTileMode.skiko.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Path.nonAndroid.kt similarity index 63% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaTileMode.skiko.kt rename to compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Path.nonAndroid.kt index af07c79681aac..53b3d9935f79d 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaTileMode.skiko.kt +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Path.nonAndroid.kt @@ -16,14 +16,9 @@ package androidx.compose.ui.graphics -import org.jetbrains.skia.FilterTileMode +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry -actual fun TileMode.isSupported(): Boolean = true - -internal fun TileMode.toSkiaTileMode(): FilterTileMode = when (this) { - TileMode.Clamp -> FilterTileMode.CLAMP - TileMode.Repeated -> FilterTileMode.REPEAT - TileMode.Mirror -> FilterTileMode.MIRROR - TileMode.Decal -> FilterTileMode.DECAL - else -> FilterTileMode.CLAMP -} +@OptIn(InternalComposeUiApi::class) +actual fun Path(): Path = + PlatformGraphicsRegistry.requireCurrent().createPath() diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/PathEffect.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/PathEffect.nonAndroid.kt new file mode 100644 index 0000000000000..bccde0e6e3ad4 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/PathEffect.nonAndroid.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry + +@OptIn(InternalComposeUiApi::class) +internal actual fun actualCornerPathEffect(radius: Float): PathEffect = + PlatformGraphicsRegistry.requireCurrent().createCornerPathEffect(radius) + +@OptIn(InternalComposeUiApi::class) +internal actual fun actualDashPathEffect( + intervals: FloatArray, + phase: Float, +): PathEffect = + PlatformGraphicsRegistry.requireCurrent().createDashPathEffect(intervals, phase) + +@OptIn(InternalComposeUiApi::class) +internal actual fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect = + PlatformGraphicsRegistry.requireCurrent().createChainPathEffect(outer, inner) + +@OptIn(InternalComposeUiApi::class) +internal actual fun actualStampedPathEffect( + shape: Path, + advance: Float, + phase: Float, + style: StampedPathEffectStyle, +): PathEffect = + PlatformGraphicsRegistry.requireCurrent().createStampedPathEffect( + shape = shape, + advance = advance, + phase = phase, + style = style, + ) diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/PathIterator.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/PathIterator.nonAndroid.kt new file mode 100644 index 0000000000000..db560ab288150 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/PathIterator.nonAndroid.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry + +@OptIn(InternalComposeUiApi::class) +actual fun PathIterator( + path: Path, + conicEvaluation: PathIterator.ConicEvaluation, + tolerance: Float +): PathIterator = + PlatformGraphicsRegistry.requireCurrent().createPathIterator( + path = path, + conicEvaluation = conicEvaluation, + tolerance = tolerance, + ) diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/PathMeasure.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/PathMeasure.nonAndroid.kt new file mode 100644 index 0000000000000..c0643d83f5ff1 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/PathMeasure.nonAndroid.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry + +@OptIn(InternalComposeUiApi::class) +actual fun PathMeasure(): PathMeasure = + PlatformGraphicsRegistry.requireCurrent().createPathMeasure() diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Shader.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Shader.nonAndroid.kt new file mode 100644 index 0000000000000..f7d683457cefe --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Shader.nonAndroid.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry +import androidx.compose.ui.graphics.platform.PlatformShader + +actual class Shader @InternalComposeUiApi constructor( + @property:InternalComposeUiApi val platformShader: PlatformShader, +) + +@OptIn(InternalComposeUiApi::class) +internal actual class TransformShader { + private var _shader: Shader? = null + private var _wrapper: Shader? = null + private var _matrix: Matrix? = null + + actual fun transform(matrix: Matrix?) { + _matrix = matrix + _wrapper = null + } + + actual var shader: Shader? + get() { + val matrix = _matrix ?: return _shader + if (_wrapper == null) { + _wrapper = _shader?.let { shader -> + PlatformGraphicsRegistry.requireCurrent() + .transformShader(shader, matrix) + } + } + return _wrapper + } + set(value) { + _shader = value + _wrapper = null + } +} + +@OptIn(InternalComposeUiApi::class) +internal actual fun ActualLinearGradientShader( + from: Offset, + to: Offset, + colors: List, + colorStops: List?, + tileMode: TileMode +): Shader = + PlatformGraphicsRegistry.requireCurrent().createLinearGradientShader( + from = from, + to = to, + colors = colors, + colorStops = colorStops, + tileMode = tileMode, + ) + +@OptIn(InternalComposeUiApi::class) +internal actual fun ActualRadialGradientShader( + center: Offset, + radius: Float, + colors: List, + colorStops: List?, + tileMode: TileMode +): Shader = + PlatformGraphicsRegistry.requireCurrent().createRadialGradientShader( + center = center, + radius = radius, + colors = colors, + colorStops = colorStops, + tileMode = tileMode, + ) + +@OptIn(InternalComposeUiApi::class) +internal actual fun ActualSweepGradientShader( + center: Offset, + colors: List, + colorStops: List? +): Shader = + PlatformGraphicsRegistry.requireCurrent().createSweepGradientShader( + center = center, + colors = colors, + colorStops = colorStops, + ) + +@OptIn(InternalComposeUiApi::class) +internal actual fun ActualImageShader( + image: ImageBitmap, + tileModeX: TileMode, + tileModeY: TileMode +): Shader = + PlatformGraphicsRegistry.requireCurrent().createImageShader( + image = image, + tileModeX = tileModeX, + tileModeY = tileModeY, + ) + +@OptIn(InternalComposeUiApi::class) +internal actual fun ActualCompositeShader(dst: Shader, src: Shader, blendMode: BlendMode): Shader = + PlatformGraphicsRegistry.requireCurrent().createCompositeShader( + destination = dst, + source = src, + blendMode = blendMode, + ) diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/TileMode.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/TileMode.nonAndroid.kt new file mode 100644 index 0000000000000..c09d15b791f95 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/TileMode.nonAndroid.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry + +@OptIn(InternalComposeUiApi::class) +actual fun TileMode.isSupported(): Boolean = + PlatformGraphicsRegistry.requireCurrent().isTileModeSupported(this) diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.nonAndroid.kt new file mode 100644 index 0000000000000..3f57af5761360 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.nonAndroid.kt @@ -0,0 +1,378 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.layer + +import androidx.annotation.IntRange +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.isUnspecified +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.RenderEffect +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.platform.PlatformGraphicsLayer +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.toSize + +@OptIn(InternalComposeUiApi::class) +actual class GraphicsLayer @InternalComposeUiApi constructor( + @property:InternalComposeUiApi val platformGraphicsLayer: PlatformGraphicsLayer, +) { + private var outlineDirty = true + private var roundRectOutlineTopLeft: Offset = Offset.Zero + private var roundRectOutlineSize: Size = Size.Unspecified + private var roundRectCornerRadius: Float = 0f + + private var internalOutline: Outline? = null + private var outlinePath: Path? = null + + /** Tracks the amount of the parent layers currently drawing this layer as a child. */ + private var parentLayerUsages = 0 + + /** Keeps track of the child layers we currently draw into this layer. */ + private val childDependenciesTracker = ChildLayerDependenciesTracker() + + actual var compositingStrategy: CompositingStrategy + get() = platformGraphicsLayer.compositingStrategy + set(value) { + if (platformGraphicsLayer.compositingStrategy != value) { + platformGraphicsLayer.compositingStrategy = value + } + } + + actual var pivotOffset: Offset + get() = platformGraphicsLayer.pivotOffset + set(value) { + if (platformGraphicsLayer.pivotOffset != value) { + platformGraphicsLayer.pivotOffset = value + } + } + + actual var alpha: Float + get() = platformGraphicsLayer.alpha + set(value) { + if (platformGraphicsLayer.alpha != value) { + platformGraphicsLayer.alpha = value + } + } + + actual var scaleX: Float + get() = platformGraphicsLayer.scaleX + set(value) { + if (platformGraphicsLayer.scaleX != value) { + platformGraphicsLayer.scaleX = value + } + } + + actual var scaleY: Float + get() = platformGraphicsLayer.scaleY + set(value) { + if (platformGraphicsLayer.scaleY != value) { + platformGraphicsLayer.scaleY = value + } + } + + actual var translationX: Float + get() = platformGraphicsLayer.translationX + set(value) { + if (platformGraphicsLayer.translationX != value) { + platformGraphicsLayer.translationX = value + } + } + + actual var translationY: Float + get() = platformGraphicsLayer.translationY + set(value) { + if (platformGraphicsLayer.translationY != value) { + platformGraphicsLayer.translationY = value + } + } + + actual var ambientShadowColor: Color + get() = platformGraphicsLayer.ambientShadowColor + set(value) { + if (platformGraphicsLayer.ambientShadowColor != value) { + platformGraphicsLayer.ambientShadowColor = value + } + } + + actual var spotShadowColor: Color + get() = platformGraphicsLayer.spotShadowColor + set(value) { + if (platformGraphicsLayer.spotShadowColor != value) { + platformGraphicsLayer.spotShadowColor = value + } + } + + actual var blendMode: BlendMode + get() = platformGraphicsLayer.blendMode + set(value) { + if (platformGraphicsLayer.blendMode != value) { + platformGraphicsLayer.blendMode = value + } + } + + actual var colorFilter: ColorFilter? + get() = platformGraphicsLayer.colorFilter + set(value) { + if (platformGraphicsLayer.colorFilter != value) { + platformGraphicsLayer.colorFilter = value + } + } + + actual var rotationX: Float + get() = platformGraphicsLayer.rotationX + set(value) { + if (platformGraphicsLayer.rotationX != value) { + platformGraphicsLayer.rotationX = value + } + } + + actual var rotationY: Float + get() = platformGraphicsLayer.rotationY + set(value) { + if (platformGraphicsLayer.rotationY != value) { + platformGraphicsLayer.rotationY = value + } + } + + actual var rotationZ: Float + get() = platformGraphicsLayer.rotationZ + set(value) { + if (platformGraphicsLayer.rotationZ != value) { + platformGraphicsLayer.rotationZ = value + } + } + + actual var cameraDistance: Float + get() = platformGraphicsLayer.cameraDistance + set(value) { + if (platformGraphicsLayer.cameraDistance != value) { + platformGraphicsLayer.cameraDistance = value + } + } + + actual var renderEffect: RenderEffect? + get() = platformGraphicsLayer.renderEffect + set(value) { + if (platformGraphicsLayer.renderEffect != value) { + platformGraphicsLayer.renderEffect = value + } + } + + actual var topLeft: IntOffset = IntOffset.Zero + set(value) { + if (field != value) { + field = value + platformGraphicsLayer.setBounds(value, size) + } + } + + actual var size: IntSize = IntSize.Zero + private set(value) { + if (field != value) { + field = value + platformGraphicsLayer.setBounds(topLeft, value) + // the layer size affects an outline which matches the layer size + if (roundRectOutlineSize.isUnspecified) { + outlineDirty = true + configureOutlineAndClip() + } + } + } + + actual var shadowElevation: Float + get() = platformGraphicsLayer.shadowElevation + set(value) { + if (platformGraphicsLayer.shadowElevation != value) { + platformGraphicsLayer.shadowElevation = value + outlineDirty = true + configureOutlineAndClip() + } + } + + actual var clip: Boolean = false + set(value) { + if (field != value) { + field = value + outlineDirty = true + configureOutlineAndClip() + } + } + + actual val outline: Outline + get() { + val tmpOutline = internalOutline + val tmpPath = outlinePath + return if (tmpOutline != null) { + tmpOutline + } else if (tmpPath != null) { + Outline.Generic(tmpPath).also { internalOutline = it } + } else { + resolveOutlinePosition { outlineTopLeft, outlineSize -> + val left = outlineTopLeft.x + val top = outlineTopLeft.y + val right = left + outlineSize.width + val bottom = top + outlineSize.height + val cornerRadius = this.roundRectCornerRadius + if (cornerRadius > 0f) { + Outline.Rounded( + RoundRect(left, top, right, bottom, CornerRadius(cornerRadius)) + ) + } else { + Outline.Rectangle(Rect(left, top, right, bottom)) + } + }.also { internalOutline = it } + } + } + + actual fun setPathOutline(path: Path) { + resetOutlineParams() + this.outlinePath = path + configureOutlineAndClip() + } + + actual fun setRoundRectOutline(topLeft: Offset, size: Size, cornerRadius: Float) { + if (this.roundRectOutlineTopLeft != topLeft || + this.roundRectOutlineSize != size || + this.roundRectCornerRadius != cornerRadius || + this.outlinePath != null + ) { + resetOutlineParams() + this.roundRectOutlineTopLeft = topLeft + this.roundRectOutlineSize = size + this.roundRectCornerRadius = cornerRadius + configureOutlineAndClip() + } + } + + actual fun setRectOutline(topLeft: Offset, size: Size) { + setRoundRectOutline(topLeft, size, 0f) + } + + actual var isReleased: Boolean = false + private set + + actual fun record( + density: Density, + layoutDirection: LayoutDirection, + size: IntSize, + block: DrawScope.() -> Unit, + ) { + this.size = size + platformGraphicsLayer.record(density, layoutDirection, this) { + childDependenciesTracker.withTracking( + onDependencyRemoved = { it.onRemovedFromParentLayer() } + ) { block() } + } + } + + actual suspend fun toImageBitmap(): ImageBitmap { + check(!isReleased) { "Cannot call toImageBitmap on a released GraphicsLayer" } + return ImageBitmap(size.width, size.height).apply { + this@GraphicsLayer.draw(Canvas(this), parentLayer = null) + } + } + + internal actual fun draw(canvas: Canvas, parentLayer: GraphicsLayer?) { + if (isReleased) return + configureOutlineAndClip() + parentLayer?.addSubLayer(this) + platformGraphicsLayer.draw(canvas) + } + + private fun addSubLayer(graphicsLayer: GraphicsLayer) { + if (childDependenciesTracker.onDependencyAdded(graphicsLayer)) { + graphicsLayer.onAddedToParentLayer() + } + } + + private fun onAddedToParentLayer() { + parentLayerUsages++ + } + + private fun onRemovedFromParentLayer() { + parentLayerUsages-- + discardContentIfReleasedAndHaveNoParentLayerUsages() + } + + private fun configureOutlineAndClip() { + if (!outlineDirty) return + val outlineIsNeeded = clip || platformGraphicsLayer.shadowElevation > 0f + platformGraphicsLayer.setOutline(if (outlineIsNeeded) outline else null, clip) + outlineDirty = false + } + + private fun resetOutlineParams() { + internalOutline = null + outlinePath = null + roundRectOutlineSize = Size.Unspecified + roundRectOutlineTopLeft = Offset.Zero + roundRectCornerRadius = 0f + outlineDirty = true + } + + private inline fun resolveOutlinePosition(block: (Offset, Size) -> T): T { + val layerSize = this.size.toSize() + val rRectTopLeft = roundRectOutlineTopLeft + val rRectSize = roundRectOutlineSize + + val outlineSize = + if (rRectSize.isUnspecified) { + layerSize + } else { + rRectSize + } + return block(rRectTopLeft, outlineSize) + } + + internal fun release() { + if (!isReleased) { + isReleased = true + discardContentIfReleasedAndHaveNoParentLayerUsages() + } + } + + private fun discardContentIfReleasedAndHaveNoParentLayerUsages() { + if (isReleased && parentLayerUsages == 0) { + // discarding means we don't draw children layer anymore and need to remove dependencies: + childDependenciesTracker.removeDependencies { it.onRemovedFromParentLayer() } + platformGraphicsLayer.discardDisplayList() + } + } + + actual fun setOutsets( + @IntRange(from = 0) left: Int, + @IntRange(from = 0) top: Int, + @IntRange(from = 0) right: Int, + @IntRange(from = 0) bottom: Int, + ) { + platformGraphicsLayer.setOutsets(left, top, right, bottom) + } +} diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformBlurFilter.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformBlurFilter.nonAndroid.kt new file mode 100644 index 0000000000000..8061821584ee3 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformBlurFilter.nonAndroid.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.platform + +import androidx.compose.ui.InternalComposeUiApi + +/** + * Opaque platform binding held by [androidx.compose.ui.graphics.shadow.BlurFilter] so ui-graphics + * stays decoupled from any concrete graphics backend. The registered backend provides the + * implementation. + */ +@InternalComposeUiApi +interface PlatformBlurFilter diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformColorFilter.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformColorFilter.nonAndroid.kt new file mode 100644 index 0000000000000..41e27c3817017 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformColorFilter.nonAndroid.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.platform + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.NativeColorFilter + +/** + * Opaque platform binding held by [ColorFilter] so ui-graphics stays decoupled from any concrete + * graphics backend. The registered backend provides the implementation and the interop extensions + * to and from its native color-filter type. + */ +@InternalComposeUiApi +interface PlatformColorFilter + +/** + * Exposes the underlying platform color-filter binding to the registered backend so it can recover + * its native object without depending on [ColorFilter]'s internal representation. + */ +@InternalComposeUiApi +val ColorFilter.platformColorFilter: PlatformColorFilter + get() = nativeColorFilter as PlatformColorFilter + +/** Wraps a platform color-filter binding into a [ColorFilter]. */ +@InternalComposeUiApi +fun PlatformColorFilter.asComposeColorFilter(): ColorFilter = ColorFilter(this) diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphics.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphics.nonAndroid.kt new file mode 100644 index 0000000000000..906e5794bcef8 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphics.nonAndroid.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.platform + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorMatrix +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.PathIterator +import androidx.compose.ui.graphics.PathMeasure +import androidx.compose.ui.graphics.RenderEffect +import androidx.compose.ui.graphics.Shader +import androidx.compose.ui.graphics.StampedPathEffectStyle +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.colorspace.ColorSpace + +/** + * Factory surface a graphics backend implements to provide ui-graphics' platform primitives. + * + * The implementation is registered at runtime, so downstream libraries compile against ui-graphics + * without depending on a specific backend. + */ +@InternalComposeUiApi +interface PlatformGraphics { + /** Creates a backend [Canvas] that draws into [image]. */ + fun createCanvas(image: ImageBitmap): Canvas + + /** Creates an empty backend [Paint]. */ + fun createPaint(): Paint + + /** Returns whether the backend can render the given [blendMode]. */ + fun isBlendModeSupported(blendMode: BlendMode): Boolean + + /** Returns whether the backend can render the given [tileMode]. */ + fun isTileModeSupported(tileMode: TileMode): Boolean + + /** Creates an empty backend [Path]. */ + fun createPath(): Path + + /** + * Creates a [PathIterator] over [path], approximating conics per [conicEvaluation] within + * [tolerance]. + */ + fun createPathIterator( + path: Path, + conicEvaluation: PathIterator.ConicEvaluation, + tolerance: Float, + ): PathIterator + + /** Creates a backend [PathMeasure]. */ + fun createPathMeasure(): PathMeasure + + /** Creates a backend [ImageBitmap] of the given dimensions and configuration. */ + fun createImageBitmap( + width: Int, + height: Int, + config: ImageBitmapConfig, + hasAlpha: Boolean, + colorSpace: ColorSpace, + ): ImageBitmap + + /** Decodes an encoded image (e.g. PNG/JPEG) from [bytes] into an [ImageBitmap]. */ + fun decodeImageBitmap(bytes: ByteArray): ImageBitmap + + /** Creates a linear-gradient [Shader] running from [from] to [to]. */ + fun createLinearGradientShader( + from: Offset, + to: Offset, + colors: List, + colorStops: List?, + tileMode: TileMode, + ): Shader + + /** Creates a radial-gradient [Shader] centered at [center] with the given [radius]. */ + fun createRadialGradientShader( + center: Offset, + radius: Float, + colors: List, + colorStops: List?, + tileMode: TileMode, + ): Shader + + /** Creates a sweep-gradient [Shader] revolving around [center]. */ + fun createSweepGradientShader( + center: Offset, + colors: List, + colorStops: List?, + ): Shader + + /** Creates a [Shader] that tiles [image] using the given per-axis tile modes. */ + fun createImageShader( + image: ImageBitmap, + tileModeX: TileMode, + tileModeY: TileMode, + ): Shader + + /** Creates a [Shader] compositing [source] over [destination] with [blendMode]. */ + fun createCompositeShader( + destination: Shader, + source: Shader, + blendMode: BlendMode, + ): Shader + + /** Returns a copy of [shader] with [matrix] applied to its local coordinate space. */ + fun transformShader(shader: Shader, matrix: Matrix): Shader + + /** + * Creates a blur [PlatformRenderEffect], optionally chained onto [renderEffect], blurring by + * [radiusX]/[radiusY] with the given [edgeTreatment]. + */ + fun createBlurRenderEffect( + renderEffect: RenderEffect?, + radiusX: Float, + radiusY: Float, + edgeTreatment: TileMode, + ): PlatformRenderEffect + + /** Creates an offset [PlatformRenderEffect], optionally chained onto [renderEffect]. */ + fun createOffsetRenderEffect(renderEffect: RenderEffect?, offset: Offset): PlatformRenderEffect + + /** + * Creates the platform blur-filter implementation used by + * [androidx.compose.ui.graphics.shadow.BlurFilter]. + * + * The returned value must be consumable by [setBlurFilter]. + */ + fun createBlurFilter(radius: Float): PlatformBlurFilter + + /** Applies [blur] (or clears it when `null`) to [paint]. */ + fun setBlurFilter(paint: Paint, blur: PlatformBlurFilter?) + + /** Creates a tint [PlatformColorFilter] applying [color] with [blendMode]. */ + fun createTintColorFilter(color: Color, blendMode: BlendMode): PlatformColorFilter + + /** Creates a [PlatformColorFilter] applying the given [colorMatrix]. */ + fun createColorMatrixColorFilter(colorMatrix: ColorMatrix): PlatformColorFilter + + /** Creates a lighting [PlatformColorFilter] that scales by [multiply] and offsets by [add]. */ + fun createLightingColorFilter(multiply: Color, add: Color): PlatformColorFilter + + /** Recovers the [ColorMatrix] backing a color-matrix [filter]. */ + fun colorMatrixFromFilter(filter: PlatformColorFilter): ColorMatrix + + /** Creates a corner-rounding [PathEffect] with the given corner [radius]. */ + fun createCornerPathEffect(radius: Float): PathEffect + + /** Creates a dashing [PathEffect] from the given [intervals] and starting [phase]. */ + fun createDashPathEffect(intervals: FloatArray, phase: Float): PathEffect + + /** Creates a [PathEffect] that applies [inner] before [outer]. */ + fun createChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect + + /** Creates a [PathEffect] that stamps [shape] along the path per [style]. */ + fun createStampedPathEffect( + shape: Path, + advance: Float, + phase: Float, + style: StampedPathEffectStyle, + ): PathEffect +} diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphicsContext.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphicsContext.nonAndroid.kt new file mode 100644 index 0000000000000..0c4f0f21cf1d6 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphicsContext.nonAndroid.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.platform + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.GraphicsContext +import androidx.compose.ui.graphics.layer.GraphicsLayer +import kotlinx.atomicfu.atomic + +/** + * Base for a [GraphicsContext] supplied by the registered graphics backend. It owns the shared + * lighting state and the active-layer bookkeeping, and defers creation of the platform layer to + * [createPlatformGraphicsLayer]. ui-graphics depends only on this abstraction, never on a concrete + * backend. + */ +@InternalComposeUiApi +abstract class PlatformGraphicsContext : GraphicsContext, AutoCloseable { + + private var isClosed = false + + private val activeGraphicsLayersCountState = atomic(0) + + /** Number of [GraphicsLayer]s created by this context that have not yet been released. */ + val activeGraphicsLayersCount: Int + get() = activeGraphicsLayersCountState.value + + /** Closes the context. Subsequent layer operations are not allowed. */ + override fun close() { + require(!isClosed) { "GraphicsContext is already closed" } + isClosed = true + } + + /** + * Configures the light source used for shadow rendering. The default implementation is a no-op; + * backends that support elevation shadows override it. + */ + open fun setLightingInfo( + centerX: Float, + centerY: Float, + centerZ: Float, + radius: Float, + ambientShadowAlpha: Float, + spotShadowAlpha: Float, + ) { + require(!isClosed) { "GraphicsContext is already closed" } + } + + /** Creates a new [GraphicsLayer] backed by [createPlatformGraphicsLayer] and tracks it. */ + override fun createGraphicsLayer(): GraphicsLayer { + require(!isClosed) { "GraphicsContext is already closed" } + activeGraphicsLayersCountState.incrementAndGet() + return GraphicsLayer(createPlatformGraphicsLayer()) + } + + /** Releases [layer] and stops tracking it against [activeGraphicsLayersCount]. */ + override fun releaseGraphicsLayer(layer: GraphicsLayer) { + if (!layer.isReleased) { + activeGraphicsLayersCountState.decrementAndGet() + } + layer.release() + } + + /** Creates the backend-specific [PlatformGraphicsLayer] that backs a [GraphicsLayer]. */ + abstract fun createPlatformGraphicsLayer(): PlatformGraphicsLayer +} diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphicsLayer.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphicsLayer.nonAndroid.kt new file mode 100644 index 0000000000000..fcab5267cba96 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphicsLayer.nonAndroid.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.platform + +import androidx.annotation.IntRange +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.RenderEffect +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.layer.CompositingStrategy +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection + +/** + * Backend-provided implementation of a [GraphicsLayer], supplied by the registered graphics backend. + * It owns the platform draw primitive and only forwards property and drawing operations; the + * layer-tree bookkeeping (outline resolution, child dependencies, release accounting) is kept in + * [GraphicsLayer] itself, mirroring the Android `GraphicsLayerImpl` split. + */ +@InternalComposeUiApi +interface PlatformGraphicsLayer { + /** Backs [GraphicsLayer.compositingStrategy]. */ + var compositingStrategy: CompositingStrategy + /** Backs [GraphicsLayer.pivotOffset]. */ + var pivotOffset: Offset + /** Backs [GraphicsLayer.alpha]. */ + var alpha: Float + /** Backs [GraphicsLayer.scaleX]. */ + var scaleX: Float + /** Backs [GraphicsLayer.scaleY]. */ + var scaleY: Float + /** Backs [GraphicsLayer.translationX]. */ + var translationX: Float + /** Backs [GraphicsLayer.translationY]. */ + var translationY: Float + /** Backs [GraphicsLayer.shadowElevation]. */ + var shadowElevation: Float + /** Backs [GraphicsLayer.ambientShadowColor]. */ + var ambientShadowColor: Color + /** Backs [GraphicsLayer.spotShadowColor]. */ + var spotShadowColor: Color + /** Backs [GraphicsLayer.blendMode]. */ + var blendMode: BlendMode + /** Backs [GraphicsLayer.colorFilter]. */ + var colorFilter: ColorFilter? + /** Backs [GraphicsLayer.rotationX]. */ + var rotationX: Float + /** Backs [GraphicsLayer.rotationY]. */ + var rotationY: Float + /** Backs [GraphicsLayer.rotationZ]. */ + var rotationZ: Float + /** Backs [GraphicsLayer.cameraDistance]. */ + var cameraDistance: Float + /** Backs [GraphicsLayer.renderEffect]. */ + var renderEffect: RenderEffect? + + /** Position the layer's content bounds. */ + fun setBounds(topLeft: IntOffset, size: IntSize) + + /** + * Apply the resolved [outline] (in layer-local coordinates) to the platform layer. A `null` + * [outline] means no clip/shadow geometry is needed. + */ + fun setOutline(outline: Outline?, clip: Boolean) + + /** Record the drawing commands of [layer] using [block]. */ + fun record( + density: Density, + layoutDirection: LayoutDirection, + layer: GraphicsLayer, + block: DrawScope.() -> Unit, + ) + + /** Draw the recorded content into [canvas]. */ + fun draw(canvas: Canvas) + + /** Release the platform draw primitive backing this layer. */ + fun discardDisplayList() + + /** @see GraphicsLayer.setOutsets */ + fun setOutsets( + @IntRange(from = 0) left: Int, + @IntRange(from = 0) top: Int, + @IntRange(from = 0) right: Int, + @IntRange(from = 0) bottom: Int, + ) +} diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphicsRegistry.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphicsRegistry.nonAndroid.kt new file mode 100644 index 0000000000000..eea3806698ad7 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformGraphicsRegistry.nonAndroid.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.platform + +import androidx.annotation.VisibleForTesting +import androidx.compose.ui.InternalComposeUiApi +import kotlinx.atomicfu.locks.SynchronizedObject +import kotlinx.atomicfu.locks.synchronized + +/** + * Process-wide registry holding the [PlatformGraphics] implementation supplied by the active + * graphics backend. ui-graphics resolves its platform primitives through [requireCurrent]; the + * backend installs itself once via [register] during its runtime initialization. + */ +@InternalComposeUiApi +object PlatformGraphicsRegistry { + private var implementation: PlatformGraphics? = null + private val lock = SynchronizedObject() + + /** + * Registers the runtime implementation once per process/classloader. + * + * Call [clear] only from tests or controlled teardown before registering a replacement. + */ + fun register(implementation: PlatformGraphics) { + synchronized(lock) { + val current = this.implementation + when { + current == null -> this.implementation = implementation + current === implementation -> Unit + else -> error( + "Compose UI graphics implementation is already registered with a different " + + "instance. Call clear() first if replacement is intentional." + ) + } + } + } + + /** Asserts that [required] is the currently registered implementation. */ + fun checkIfRegistered(required: PlatformGraphics) = + check(required === implementation) { + "Registered implementation is $implementation, but required is $required" + } + + internal fun requireCurrent(): PlatformGraphics = + implementation ?: error("No Compose UI graphics implementation is registered.") + + /** + * Clears the current implementation. + * + * Intended for tests or controlled teardown only. + */ + @VisibleForTesting + fun clear() { + implementation = null + } +} diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformRenderEffect.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformRenderEffect.nonAndroid.kt new file mode 100644 index 0000000000000..57369dc37d190 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformRenderEffect.nonAndroid.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.platform + +import androidx.compose.ui.InternalComposeUiApi + +/** + * Opaque platform binding held by [androidx.compose.ui.graphics.RenderEffect] so ui-graphics stays + * decoupled from any concrete graphics backend. The registered backend provides the implementation + * and the interop extensions to and from its native image-filter type. + */ +@InternalComposeUiApi +interface PlatformRenderEffect diff --git a/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformShader.nonAndroid.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformShader.nonAndroid.kt new file mode 100644 index 0000000000000..9da374502daa2 --- /dev/null +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/platform/PlatformShader.nonAndroid.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.platform + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.Shader + +/** + * Opaque platform binding held by [Shader] so ui-graphics stays decoupled from any concrete + * graphics backend. The registered backend provides the implementation and the interop extensions + * to and from its native shader type. + */ +@InternalComposeUiApi +interface PlatformShader diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.nonAndroid.kt similarity index 59% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt rename to compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.nonAndroid.kt index 488847530cb36..2c97d20e890f9 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt +++ b/compose/ui/ui-graphics/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.nonAndroid.kt @@ -17,22 +17,21 @@ package androidx.compose.ui.graphics.shadow import androidx.compose.ui.InternalComposeUiApi -import androidx.compose.ui.graphics.BlurEffect.Companion.convertRadiusToSigma import androidx.compose.ui.graphics.Paint -import androidx.compose.ui.graphics.skiaPaint -import org.jetbrains.skia.FilterBlurMode -import org.jetbrains.skia.MaskFilter +import androidx.compose.ui.graphics.platform.PlatformBlurFilter +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry -@OptIn(InternalComposeUiApi::class) -internal actual fun BlurFilter(radius: Float): BlurFilter { - val sigma = convertRadiusToSigma(radius); - return MaskFilter.makeBlur(FilterBlurMode.NORMAL, sigma) -} +// Use `Any` to avoid double-wrapping and EXPECT_ACTUAL_INCOMPATIBLE_CLASS_KIND +internal actual typealias BlurFilter = Any // == PlatformBlurFilter -// TODO: Do not expose skiko types to common -// https://youtrack.jetbrains.com/issue/CMP-219 -internal actual typealias BlurFilter = MaskFilter // Only the base type is available +@OptIn(InternalComposeUiApi::class) +internal actual fun BlurFilter(radius: Float): BlurFilter = + PlatformGraphicsRegistry.requireCurrent().createBlurFilter(radius) +@OptIn(InternalComposeUiApi::class) internal actual fun Paint.setBlurFilter(blur: BlurFilter?) { - skiaPaint.maskFilter = blur + PlatformGraphicsRegistry.requireCurrent().setBlurFilter( + paint = this, + blur = blur as PlatformBlurFilter?, + ) } diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/BlendMode.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/BlendMode.skiko.kt deleted file mode 100644 index 4bd388214ae82..0000000000000 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/BlendMode.skiko.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.compose.ui.graphics - -internal fun BlendMode.toSkia() = when (this) { - BlendMode.Clear -> org.jetbrains.skia.BlendMode.CLEAR - BlendMode.Src -> org.jetbrains.skia.BlendMode.SRC - BlendMode.Dst -> org.jetbrains.skia.BlendMode.DST - BlendMode.SrcOver -> org.jetbrains.skia.BlendMode.SRC_OVER - BlendMode.DstOver -> org.jetbrains.skia.BlendMode.DST_OVER - BlendMode.SrcIn -> org.jetbrains.skia.BlendMode.SRC_IN - BlendMode.DstIn -> org.jetbrains.skia.BlendMode.DST_IN - BlendMode.SrcOut -> org.jetbrains.skia.BlendMode.SRC_OUT - BlendMode.DstOut -> org.jetbrains.skia.BlendMode.DST_OUT - BlendMode.SrcAtop -> org.jetbrains.skia.BlendMode.SRC_ATOP - BlendMode.DstAtop -> org.jetbrains.skia.BlendMode.DST_ATOP - BlendMode.Xor -> org.jetbrains.skia.BlendMode.XOR - BlendMode.Plus -> org.jetbrains.skia.BlendMode.PLUS - BlendMode.Modulate -> org.jetbrains.skia.BlendMode.MODULATE - BlendMode.Screen -> org.jetbrains.skia.BlendMode.SCREEN - BlendMode.Overlay -> org.jetbrains.skia.BlendMode.OVERLAY - BlendMode.Darken -> org.jetbrains.skia.BlendMode.DARKEN - BlendMode.Lighten -> org.jetbrains.skia.BlendMode.LIGHTEN - BlendMode.ColorDodge -> org.jetbrains.skia.BlendMode.COLOR_DODGE - BlendMode.ColorBurn -> org.jetbrains.skia.BlendMode.COLOR_BURN - BlendMode.Hardlight -> org.jetbrains.skia.BlendMode.HARD_LIGHT - BlendMode.Softlight -> org.jetbrains.skia.BlendMode.SOFT_LIGHT - BlendMode.Difference -> org.jetbrains.skia.BlendMode.DIFFERENCE - BlendMode.Exclusion -> org.jetbrains.skia.BlendMode.EXCLUSION - BlendMode.Multiply -> org.jetbrains.skia.BlendMode.MULTIPLY - BlendMode.Hue -> org.jetbrains.skia.BlendMode.HUE - BlendMode.Saturation -> org.jetbrains.skia.BlendMode.SATURATION - BlendMode.Color -> org.jetbrains.skia.BlendMode.COLOR - BlendMode.Luminosity -> org.jetbrains.skia.BlendMode.LUMINOSITY - // Always fallback to default blendmode of src over - else -> org.jetbrains.skia.BlendMode.SRC_OVER -} \ No newline at end of file diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Canvas.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Canvas.skiko.kt new file mode 100644 index 0000000000000..cc0322895e535 --- /dev/null +++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Canvas.skiko.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import org.jetbrains.skia.Canvas as SkCanvas + +@Deprecated( + message = "Use direct reference to org.jetbrains.skia.Canvas instead of typealias", + replaceWith = ReplaceWith("Canvas", "org.jetbrains.skia.Canvas"), + level = DeprecationLevel.ERROR, +) +actual typealias NativeCanvas = SkCanvas diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Matrices.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Matrices.skiko.kt deleted file mode 100644 index 81562721743ba..0000000000000 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Matrices.skiko.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.compose.ui.graphics - -import androidx.compose.ui.InternalComposeUiApi -import kotlin.math.abs -import org.jetbrains.skia.Matrix33 - -internal fun identityMatrix33() = Matrix33( - 1f, 0f, 0f, - 0f, 1f, 0f, - 0f, 0f, 1f -) - -internal fun Matrix.setFrom(matrix: Matrix33) { - val v = values - val m = matrix.mat - val scaleX = m[0] // MSCALE_X - val skewX = m[1] // MSKEW_X - val translateX = m[2] // MTRANS_X - val skewY = m[3] // MSKEW_Y - val scaleY = m[4] // MSCALE_Y - val translateY = m[5] // MTRANS_Y - val persp0 = m[6] // MPERSP_0 - val persp1 = m[7] // MPERSP_1 - val persp2 = m[8] // MPERSP_2 - - v[Matrix.ScaleX] = scaleX // 0 - v[Matrix.SkewY] = skewY // 1 - v[2] = 0f // 2 - v[Matrix.Perspective0] = persp0 // 3 - v[Matrix.SkewX] = skewX // 4 - v[Matrix.ScaleY] = scaleY // 5 - v[6] = 0f // 6 - v[Matrix.Perspective1] = persp1 // 7 - v[8] = 0f // 8 - v[9] = 0f // 9 - v[Matrix.ScaleZ] = 1.0f // 10 - v[11] = 0f // 11 - v[Matrix.TranslateX] = translateX // 12 - v[Matrix.TranslateY] = translateY // 13 - v[14] = 0f // 14 - v[Matrix.Perspective2] = persp2 // 15 -} - -internal fun Matrix33.setFrom(matrix: Matrix) { - // We'll reuse the array used in Matrix to avoid allocation by temporarily - // setting it to the 3x3 matrix used by android.graphics.Matrix - // Store the values of the 4 x 4 matrix into temporary variables - // to be reset after the 3 x 3 matrix is configured - val scaleX = matrix.values[Matrix.ScaleX] // 0 - val skewY = matrix.values[Matrix.SkewY] // 1 - val v2 = matrix.values[2] // 2 - val persp0 = matrix.values[Matrix.Perspective0] // 3 - val skewX = matrix.values[Matrix.SkewX] // 4 - val scaleY = matrix.values[Matrix.ScaleY] // 5 - val v6 = matrix.values[6] // 6 - val persp1 = matrix.values[Matrix.Perspective1] // 7 - val v8 = matrix.values[8] // 8 - - val translateX = matrix.values[Matrix.TranslateX] - val translateY = matrix.values[Matrix.TranslateY] - val persp2 = matrix.values[Matrix.Perspective2] - - val v = matrix.values - - v[0] = scaleX // MSCALE_X = 0 - v[1] = skewX // MSKEW_X = 1 - v[2] = translateX // MTRANS_X = 2 - v[3] = skewY // MSKEW_Y = 3 - v[4] = scaleY // MSCALE_Y = 4 - v[5] = translateY // MTRANS_Y - v[6] = persp0 // MPERSP_0 = 6 - v[7] = persp1 // MPERSP_1 = 7 - v[8] = persp2 // MPERSP_2 = 8 - - for (i in 0..8) { - mat[i] = v[i] - } - - // Reset the values back after the android.graphics.Matrix is configured - v[Matrix.ScaleX] = scaleX // 0 - v[Matrix.SkewY] = skewY // 1 - v[2] = v2 // 2 - v[Matrix.Perspective0] = persp0 // 3 - v[Matrix.SkewX] = skewX // 4 - v[Matrix.ScaleY] = scaleY // 5 - v[6] = v6 // 6 - v[Matrix.Perspective1] = persp1 // 7 - v[8] = v8 // 8 -} - -@InternalComposeUiApi -fun prepareTransformationMatrix( - matrix: Matrix, - pivotX: Float, - pivotY: Float, - translationX: Float, - translationY: Float, - rotationX: Float, - rotationY: Float, - rotationZ: Float, - scaleX: Float, - scaleY: Float, - cameraDistance: Float, -) { - matrix.reset() - matrix.translate(x = -pivotX, y = -pivotY) - matrix *= Matrix().apply { - rotateZ(rotationZ) - rotateY(rotationY) - rotateX(rotationX) - scale(scaleX, scaleY) - } - // Perspective transform should be applied only in case of rotations to avoid - // multiply application in hierarchies. - // See Android's frameworks/base/libs/hwui/RenderProperties.cpp for reference - if (!rotationX.isZero() || !rotationY.isZero()) { - matrix *= Matrix().apply { - // The camera location is passed in inches, set in pt - val depth = cameraDistance * 72f - this[2, 3] = -1f / depth - } - } - matrix *= Matrix().apply { - translate(x = pivotX + translationX, y = pivotY + translationY) - } - - // Third column and row are irrelevant for 2D space. - // Zeroing required to get correct inverse transformation matrix. - matrix[2, 0] = 0f - matrix[2, 1] = 0f - matrix[2, 3] = 0f - matrix[0, 2] = 0f - matrix[1, 2] = 0f - matrix[3, 2] = 0f -} - -// Copy from Android's frameworks/base/libs/hwui/utils/MathUtils.h -private const val NON_ZERO_EPSILON = 0.001f - -@Suppress("NOTHING_TO_INLINE") -private inline fun Float.isZero(): Boolean = abs(this) <= NON_ZERO_EPSILON diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Paint.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Paint.skiko.kt new file mode 100644 index 0000000000000..6f131c6b1f967 --- /dev/null +++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Paint.skiko.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import org.jetbrains.skia.Paint as SkPaint + +@Deprecated( + message = "Use org.jetbrains.skia.Paint directly instead", + replaceWith = ReplaceWith("org.jetbrains.skia.Paint"), + level = DeprecationLevel.ERROR, +) +actual typealias NativePaint = SkPaint diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/RenderEffect.skiko.kt similarity index 58% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.skiko.kt rename to compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/RenderEffect.skiko.kt index 8e056eea4d7c9..5cfcc2e3c49c6 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.skiko.kt +++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/RenderEffect.skiko.kt @@ -19,22 +19,10 @@ package androidx.compose.ui.graphics import androidx.compose.runtime.Immutable import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry +import androidx.compose.ui.graphics.platform.PlatformRenderEffect import org.jetbrains.skia.ImageFilter -/** - * Convert the [org.jetbrains.skia.ImageFilter] instance into a Compose-compatible [RenderEffect] - */ -fun ImageFilter.asComposeRenderEffect(): RenderEffect = - SkiaBackedRenderEffect(this) - -/** - * Provides access to the underlying [org.jetbrains.skia.ImageFilter] instance. - * - * It throws an exception if accessed on unsupported types. - */ -val RenderEffect.skiaImageFilter: ImageFilter - get() = internalSkiaImageFilter - /** * Intermediate rendering step used to render drawing commands with a corresponding * visual effect. A [RenderEffect] can be configured on a [GraphicsLayerScope] @@ -43,18 +31,18 @@ val RenderEffect.skiaImageFilter: ImageFilter @Immutable actual sealed class RenderEffect actual constructor() { - private var _internalSkiaImageFilter: ImageFilter? = null - internal val internalSkiaImageFilter: ImageFilter - get() = _internalSkiaImageFilter ?: createImageFilter().also { _internalSkiaImageFilter = it } + @InternalComposeUiApi + abstract val platformRenderEffect: PlatformRenderEffect @Deprecated( message = "Use [RenderEffect.skiaImageFilter] extension instead", replaceWith = ReplaceWith("skiaImageFilter", "androidx.compose.ui.graphics.skiaImageFilter"), level = DeprecationLevel.ERROR, ) - fun asSkiaImageFilter(): ImageFilter = internalSkiaImageFilter - - protected abstract fun createImageFilter(): ImageFilter + @Suppress("DEPRECATION") + @OptIn(InternalComposeUiApi::class) + fun asSkiaImageFilter(): ImageFilter = + SkikoGraphicsCompatRegistry.requireCurrent().imageFilter(platformRenderEffect) /** * Capability query to determine if the particular platform supports the [RenderEffect]. Not @@ -64,11 +52,14 @@ actual sealed class RenderEffect actual constructor() { } @Immutable -internal class SkiaBackedRenderEffect( - val imageFilter: ImageFilter -) : RenderEffect() { - override fun createImageFilter(): ImageFilter = imageFilter -} +@OptIn(InternalComposeUiApi::class) +internal class CustomPlatformRenderEffect( + override val platformRenderEffect: PlatformRenderEffect, +) : RenderEffect() + +/** Wraps a platform render-effect binding into a [RenderEffect]. */ +@InternalComposeUiApi +fun PlatformRenderEffect.asComposeRenderEffect(): RenderEffect = CustomPlatformRenderEffect(this) @Immutable actual class BlurEffect actual constructor( @@ -78,23 +69,11 @@ actual class BlurEffect actual constructor( private val edgeTreatment: TileMode ) : RenderEffect() { - @OptIn(InternalComposeUiApi::class) - override fun createImageFilter(): ImageFilter = - if (renderEffect == null) { - ImageFilter.makeBlur( - convertRadiusToSigma(radiusX), - convertRadiusToSigma(radiusY), - edgeTreatment.toSkiaTileMode() - ) - } else { - ImageFilter.makeBlur( - convertRadiusToSigma(radiusX), - convertRadiusToSigma(radiusY), - edgeTreatment.toSkiaTileMode(), - renderEffect.skiaImageFilter, - null - ) - } + @InternalComposeUiApi + override val platformRenderEffect: PlatformRenderEffect by lazy { + PlatformGraphicsRegistry.requireCurrent() + .createBlurRenderEffect(renderEffect, radiusX, radiusY, edgeTreatment) + } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -121,23 +100,7 @@ actual class BlurEffect actual constructor( "edgeTreatment=$edgeTreatment)" } - companion object { - - // Constant used to convert blur radius into a corresponding sigma value - // for the gaussian blur algorithm used within SkImageFilter. - // This constant approximates the scaling done in the software path's - // "high quality" mode, in SkBlurMask::Blur() (1 / sqrt(3)). - @InternalComposeUiApi // Never supposed to be used public. Will be hidden in future versions - val BlurSigmaScale = 0.57735f - - @InternalComposeUiApi // Never supposed to be used public. Will be hidden in future versions - fun convertRadiusToSigma(radius: Float) = - if (radius > 0) { - BlurSigmaScale * radius + 0.5f - } else { - 0.0f - } - } + companion object } @Immutable @@ -146,8 +109,11 @@ actual class OffsetEffect actual constructor( private val offset: Offset ) : RenderEffect() { - override fun createImageFilter(): ImageFilter = - ImageFilter.makeOffset(offset.x, offset.y, renderEffect?.skiaImageFilter, null) + @InternalComposeUiApi + override val platformRenderEffect: PlatformRenderEffect by lazy { + PlatformGraphicsRegistry.requireCurrent() + .createOffsetRenderEffect(renderEffect, offset) + } override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.skiko.kt deleted file mode 100644 index 27a1772cdf304..0000000000000 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.skiko.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.compose.ui.graphics - -import org.jetbrains.skia.ColorFilter as SkColorFilter -import org.jetbrains.skia.ColorMatrix as SkColorMatrix - -internal actual typealias NativeColorFilter = SkColorFilter - -/** - * Obtain a [org.jetbrains.skia.ColorFilter] instance from this [ColorFilter] - */ -fun ColorFilter.asSkiaColorFilter(): SkColorFilter = nativeColorFilter - -/** - * Create a [ColorFilter] from the given [org.jetbrains.skia.ColorFilter] instance - */ -fun SkColorFilter.asComposeColorFilter(): ColorFilter = ColorFilter(this) - -internal actual fun actualTintColorFilter(color: Color, blendMode: BlendMode): NativeColorFilter = - SkColorFilter.makeBlend(color.toArgb(), blendMode.toSkia()) - -/** - * Remaps compose [ColorMatrix] to [org.jetbrains.skia.ColorMatrix] and returns [ColorFilter] - * applying this matrix to draw color result - */ -internal actual fun actualColorMatrixColorFilter(colorMatrix: ColorMatrix): NativeColorFilter { - val remappedValues = colorMatrix.values.copyOf() - remappedValues[4] *= (1f / 255f) - remappedValues[9] *= (1f / 255f) - remappedValues[14] *= (1f / 255f) - remappedValues[19] *= (1f / 255f) - - return SkColorFilter.makeMatrix( - SkColorMatrix(remappedValues) - ) -} - -internal actual fun actualLightingColorFilter(multiply: Color, add: Color): NativeColorFilter = - SkColorFilter.makeLighting(multiply.toArgb(), add.toArgb()) - -// TODO: https://youtrack.jetbrains.com/issue/CMP-739 -internal actual fun actualColorMatrixFromFilter(filter: NativeColorFilter): ColorMatrix = - ColorMatrix() diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaGraphicsContext.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaGraphicsContext.skiko.kt deleted file mode 100644 index 405c5356b483e..0000000000000 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaGraphicsContext.skiko.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.compose.ui.graphics - -import androidx.compose.ui.InternalComposeUiApi -import androidx.compose.ui.graphics.layer.GraphicsLayer -import org.jetbrains.skiko.node.RenderNode -import org.jetbrains.skiko.node.RenderNodeContext - -@InternalComposeUiApi -class SkiaGraphicsContext( - measureDrawBounds: Boolean = false, -): GraphicsContext { - private val renderNodeContext = RenderNodeContext( - measureDrawBounds = measureDrawBounds, - ) - private var isClosed = false - - // Temporary workaround to disable state tracking workaround inside old internal layers - var activeGraphicsLayersCount = 0 - private set - - fun dispose() { - require(!isClosed) { "GraphicsContext is already closed" } - isClosed = true - renderNodeContext.close() - } - - fun setLightingInfo( - centerX: Float = Float.MIN_VALUE, - centerY: Float = Float.MIN_VALUE, - centerZ: Float = Float.MIN_VALUE, - radius: Float = 0f, - ambientShadowAlpha: Float = 0f, - spotShadowAlpha: Float = 0f - ) { - require(!isClosed) { "GraphicsContext is already closed" } - renderNodeContext.setLightingInfo( - centerX, - centerY, - centerZ, - radius, - ambientShadowAlpha, - spotShadowAlpha - ) - } - - override fun createGraphicsLayer(): GraphicsLayer { - require(!isClosed) { "GraphicsContext is already closed" } - activeGraphicsLayersCount++ - return GraphicsLayer( - renderNode = RenderNode(renderNodeContext) - ) - } - - override fun releaseGraphicsLayer(layer: GraphicsLayer) { - if (!layer.isReleased) { - activeGraphicsLayersCount-- - } - layer.release() - } -} diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaShader.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaShader.skiko.kt deleted file mode 100644 index 6cadef57dd047..0000000000000 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaShader.skiko.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.compose.ui.graphics - -import androidx.compose.ui.geometry.Offset -import org.jetbrains.skia.Color4f -import org.jetbrains.skia.Gradient -import org.jetbrains.skia.Matrix33 -import org.jetbrains.skia.Shader as SkShader - -actual class Shader internal constructor( - internal val internalSkiaShader: SkShader, -) - -/** - * Convert the [org.jetbrains.skia.Shader] instance into a Compose-compatible Shader - */ -fun SkShader.asComposeShader(): Shader = Shader(internalSkiaShader = this) - -/** - * Provides access to the underlying [org.jetbrains.skia.Shader] instance. - */ -val Shader.skiaShader: SkShader - get() = internalSkiaShader - -internal actual class TransformShader { - private var _shader: Shader? = null - private var _wrapper: Shader? = null - private var _matrix: Matrix33? = null - - actual fun transform(matrix: Matrix?) { - _matrix = if (matrix != null) { - Matrix33.makeTranslate(0f, 0f).apply { setFrom(matrix) } - } else null - _wrapper = null - } - - actual var shader: Shader? - get() { - val matrix = _matrix ?: return _shader - if (_wrapper == null) { - _wrapper = _shader - ?.skiaShader - ?.makeWithLocalMatrix(matrix) - ?.asComposeShader() - } - return _wrapper - } - set(value) { - _shader = value - _wrapper = null - } -} - -internal actual fun ActualLinearGradientShader( - from: Offset, - to: Offset, - colors: List, - colorStops: List?, - tileMode: TileMode -): Shader { - validateColorStops(colors, colorStops) - return SkShader.makeLinearGradient( - from.x, - from.y, - to.x, - to.y, - colors.toSkiaGradient( - colorStops = colorStops, - tileMode = tileMode - ) - ).asComposeShader() -} - -internal actual fun ActualRadialGradientShader( - center: Offset, - radius: Float, - colors: List, - colorStops: List?, - tileMode: TileMode -): Shader { - validateColorStops(colors, colorStops) - return SkShader.makeRadialGradient( - center.x, - center.y, - radius, - colors.toSkiaGradient( - colorStops = colorStops, - tileMode = tileMode - ) - ).asComposeShader() -} - -internal actual fun ActualSweepGradientShader( - center: Offset, - colors: List, - colorStops: List? -): Shader { - validateColorStops(colors, colorStops) - return SkShader.makeSweepGradient( - center.x, - center.y, - colors.toSkiaGradient(colorStops = colorStops) - ).asComposeShader() -} - -internal actual fun ActualImageShader( - image: ImageBitmap, - tileModeX: TileMode, - tileModeY: TileMode -): Shader { - return image.asSkiaBitmap().makeShader( - tileModeX.toSkiaTileMode(), - tileModeY.toSkiaTileMode() - ).asComposeShader() -} - -internal actual fun ActualCompositeShader(dst: Shader, src: Shader, blendMode: BlendMode): Shader = - SkShader.makeBlend( - mode = blendMode.toSkia(), - dst = dst.skiaShader, - src = src.skiaShader - ).asComposeShader() - -private fun List.toSkiaGradient( - colorStops: List?, - tileMode: TileMode = TileMode.Clamp -): Gradient = Gradient( - colors = Gradient.Colors( - colors = toColor4fArray(), - positions = colorStops?.toFloatArray(), - tileMode = tileMode.toSkiaTileMode() - ), - interpolation = Gradient.Interpolation( - inPremul = Gradient.Interpolation.InPremul.YES - ) -) - -private fun List.toColor4fArray(): Array = - Array(size) { i -> - val color = this[i] - Color4f(color.red, color.green, color.blue, color.alpha) - } - -private fun validateColorStops(colors: List, colorStops: List?) { - if (colorStops == null) { - if (colors.size < 2) { - throw IllegalArgumentException( - "colors must have length of at least 2 if colorStops " + - "is omitted." - ) - } - } else if (colors.size != colorStops.size) { - throw IllegalArgumentException( - "colors and colorStops arguments must have" + - " equal length." - ) - } -} diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkikoGraphicsCompat.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkikoGraphicsCompat.skiko.kt new file mode 100644 index 0000000000000..c0fcedfca6d41 --- /dev/null +++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkikoGraphicsCompat.skiko.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformRenderEffect +import org.jetbrains.skia.ImageFilter + +/** + * TEMPORARY compatibility bridge for deprecated Skia-returning **members** that cannot be expressed + * as relocatable extension functions (and therefore cannot be moved to `:compose:ui:ui-skiko`): + * currently [RenderEffect.asSkiaImageFilter]. + * + * The implementation is registered by `:compose:ui:ui-skiko` so the real Skia work still lives there. + * This interface exists only to keep those already-deprecated members working and is expected to be + * removed when they are dropped — prefer the [skiaImageFilter] extension instead. + */ +@Deprecated("It's used only for deprecated compatibility support and will be removed in the future.") +@InternalComposeUiApi +interface SkikoGraphicsCompat { + fun imageFilter(renderEffect: PlatformRenderEffect): ImageFilter +} + +@Deprecated("It's used only for deprecated compatibility support and will be removed in the future.") +@Suppress("DEPRECATION") +@InternalComposeUiApi +object SkikoGraphicsCompatRegistry { + private var current: SkikoGraphicsCompat? = null + + fun register(value: SkikoGraphicsCompat) { + current = value + } + + fun requireCurrent(): SkikoGraphicsCompat = + current ?: error("No Skiko graphics compat is registered.") + + fun clear() { + current = null + } +} diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt deleted file mode 100644 index dbcce8d2f5089..0000000000000 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt +++ /dev/null @@ -1,477 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.compose.ui.graphics.layer - -import androidx.annotation.IntRange -import androidx.compose.ui.InternalComposeUiApi -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.RoundRect -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.geometry.isUnspecified -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Canvas -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.Outline -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.RenderEffect -import androidx.compose.ui.graphics.SkiaBackedCanvas -import androidx.compose.ui.graphics.asComposeCanvas -import androidx.compose.ui.graphics.asSkiaColorFilter -import androidx.compose.ui.graphics.drawscope.CanvasDrawScope -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.draw -import androidx.compose.ui.graphics.skiaCanvas -import androidx.compose.ui.graphics.skiaImageFilter -import androidx.compose.ui.graphics.materializeSkiaPath -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.graphics.toSkia -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.toSize -import org.jetbrains.skia.Paint as SkPaint -import org.jetbrains.skia.Point -import org.jetbrains.skia.Rect as SkRect -import org.jetbrains.skiko.node.RenderNode - -actual class GraphicsLayer internal constructor( - renderNode: RenderNode, -) { - private var renderNode: RenderNode? = renderNode - private val pictureDrawScope = CanvasDrawScope() - - private var outlineDirty = true - private var roundRectOutlineTopLeft: Offset = Offset.Zero - private var roundRectOutlineSize: Size = Size.Unspecified - private var roundRectCornerRadius: Float = 0f - - private var internalOutline: Outline? = null - private var outlinePath: Path? = null - - private var parentLayerUsages = 0 - private val childDependenciesTracker = ChildLayerDependenciesTracker() - - actual var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto - set(value) { - if (field != value) { - field = value - updateLayerProperties() - } - } - - actual var topLeft: IntOffset = IntOffset.Zero - set(value) { - if (field != value) { - field = value - renderNode?.bounds = SkRect.makeXYWH( - value.x.toFloat(), - value.y.toFloat(), - size.width.toFloat(), - size.height.toFloat() - ) - } - } - - actual var size: IntSize = IntSize.Zero - private set(value) { - if (field != value) { - field = value - renderNode?.bounds = SkRect.makeXYWH( - topLeft.x.toFloat(), - topLeft.y.toFloat(), - value.width.toFloat(), - value.height.toFloat() - ) - if (roundRectOutlineSize.isUnspecified) { - outlineDirty = true - configureOutlineAndClip() - } - } - } - - actual var pivotOffset: Offset = Offset.Unspecified - set(value) { - if (field != value) { - field = value - renderNode?.pivot = Point(value.x, value.y) - } - } - - actual var alpha: Float = 1f - set(value) { - if (field != value) { - field = value - renderNode?.alpha = value - updateLayerProperties() - } - } - - actual var scaleX: Float = 1f - set(value) { - if (field != value) { - field = value - renderNode?.scaleX = value - } - } - - actual var scaleY: Float = 1f - set(value) { - if (field != value) { - field = value - renderNode?.scaleY = value - } - } - - actual var translationX: Float = 0f - set(value) { - if (field != value) { - field = value - renderNode?.translationX = value - } - } - actual var translationY: Float = 0f - set(value) { - if (field != value) { - field = value - renderNode?.translationY = value - } - } - - actual var shadowElevation: Float = 0f - set(value) { - if (field != value) { - field = value - renderNode?.shadowElevation = value - outlineDirty = true - configureOutlineAndClip() - } - } - - actual var ambientShadowColor: Color = Color.Black - set(value) { - if (field != value) { - field = value - renderNode?.ambientShadowColor = value.toArgb() - } - } - - actual var spotShadowColor: Color = Color.Black - set(value) { - if (field != value) { - field = value - renderNode?.spotShadowColor = value.toArgb() - } - } - - actual var blendMode: BlendMode = BlendMode.SrcOver - set(value) { - if (field != value) { - field = value - updateLayerProperties() - } - } - - actual var colorFilter: ColorFilter? = null - set(value) { - if (field != value) { - field = value - updateLayerProperties() - } - } - - actual val outline: Outline - get() { - val tmpOutline = internalOutline - val tmpPath = outlinePath - return if (tmpOutline != null) { - tmpOutline - } else if (tmpPath != null) { - Outline.Generic(tmpPath).also { internalOutline = it } - } else { - resolveOutlinePosition { outlineTopLeft, outlineSize -> - val left = outlineTopLeft.x - val top = outlineTopLeft.y - val right = left + outlineSize.width - val bottom = top + outlineSize.height - val cornerRadius = this.roundRectCornerRadius - if (cornerRadius > 0f) { - Outline.Rounded( - RoundRect(left, top, right, bottom, CornerRadius(cornerRadius)) - ) - } else { - Outline.Rectangle(Rect(left, top, right, bottom)) - } - }.also { internalOutline = it } - } - } - - private fun resetOutlineParams() { - internalOutline = null - outlinePath = null - roundRectOutlineSize = Size.Unspecified - roundRectOutlineTopLeft = Offset.Zero - roundRectCornerRadius = 0f - outlineDirty = true - } - - actual fun setPathOutline(path: Path) { - resetOutlineParams() - this.outlinePath = path - configureOutlineAndClip() - } - - actual fun setRoundRectOutline(topLeft: Offset, size: Size, cornerRadius: Float) { - if (this.roundRectOutlineTopLeft != topLeft || - this.roundRectOutlineSize != size || - this.roundRectCornerRadius != cornerRadius || - this.outlinePath != null - ) { - resetOutlineParams() - this.roundRectOutlineTopLeft = topLeft - this.roundRectOutlineSize = size - this.roundRectCornerRadius = cornerRadius - configureOutlineAndClip() - } - } - - actual fun setRectOutline(topLeft: Offset, size: Size) { - setRoundRectOutline(topLeft, size, 0f) - } - - actual var rotationX: Float = 0f - set(value) { - if (field != value) { - field = value - renderNode?.rotationX = value - } - } - - actual var rotationY: Float = 0f - set(value) { - if (field != value) { - field = value - renderNode?.rotationY = value - } - } - - actual var rotationZ: Float = 0f - set(value) { - if (field != value) { - field = value - renderNode?.rotationZ = value - } - } - - actual var cameraDistance: Float = DefaultCameraDistance - set(value) { - if (field != value) { - field = value - renderNode?.cameraDistance = value - } - } - - actual var clip: Boolean = false - set(value) { - if (field != value) { - field = value - outlineDirty = true - configureOutlineAndClip() - } - } - - actual var renderEffect: RenderEffect? = null - set(value) { - if (field != value) { - field = value - updateLayerProperties() - } - } - - actual var isReleased: Boolean = false - private set - - actual fun record( - density: Density, - layoutDirection: LayoutDirection, - size: IntSize, - block: DrawScope.() -> Unit - ) { - this.size = size - recordWithTracking { canvas -> - canvas.alphaMultiplier = if (compositingStrategy == CompositingStrategy.ModulateAlpha) { - this@GraphicsLayer.alpha - } else { - 1.0f - } - pictureDrawScope.draw( - density = density, - layoutDirection = layoutDirection, - canvas = canvas, - size = size.toSize(), - graphicsLayer = this, - block = block - ) - } - } - - private fun recordWithTracking(block: (SkiaBackedCanvas) -> Unit) { - val renderNode = renderNode ?: return - val recordingCanvas = renderNode.beginRecording() - try { - val composeCanvas = recordingCanvas.asComposeCanvas() as SkiaBackedCanvas - childDependenciesTracker.withTracking( - onDependencyRemoved = { it.onRemovedFromParentLayer() }, - ) { block(composeCanvas) } - } finally { - renderNode.endRecording() - } - } - - private fun addSubLayer(graphicsLayer: GraphicsLayer) { - if (childDependenciesTracker.onDependencyAdded(graphicsLayer)) { - graphicsLayer.onAddedToParentLayer() - } - } - - internal actual fun draw(canvas: Canvas, parentLayer: GraphicsLayer?) { - if (isReleased) return - configureOutlineAndClip() - parentLayer?.addSubLayer(this) - renderNode?.drawInto(canvas.skiaCanvas) - } - - private fun onAddedToParentLayer() { - parentLayerUsages++ - } - - private fun onRemovedFromParentLayer() { - parentLayerUsages-- - discardContentIfReleasedAndHaveNoParentLayerUsages() - } - - @OptIn(InternalComposeUiApi::class) - private fun configureOutlineAndClip() { - if (!outlineDirty) return - val renderNode = renderNode ?: return - val outlineIsNeeded = clip || shadowElevation > 0f - if (!outlineIsNeeded) { - renderNode.clip = false - renderNode.setClipPath(null) - } else { - renderNode.clip = clip - when (val tmpOutline = outline) { - is Outline.Rectangle -> renderNode.setClipRect( - tmpOutline.rect.left, - tmpOutline.rect.top, - tmpOutline.rect.right, - tmpOutline.rect.bottom, - antiAlias = true - ) - is Outline.Rounded -> renderNode.setClipRRect( - tmpOutline.roundRect.left, - tmpOutline.roundRect.top, - tmpOutline.roundRect.right, - tmpOutline.roundRect.bottom, - floatArrayOf( - tmpOutline.roundRect.topLeftCornerRadius.x, - tmpOutline.roundRect.topLeftCornerRadius.y, - tmpOutline.roundRect.topRightCornerRadius.x, - tmpOutline.roundRect.topRightCornerRadius.y, - tmpOutline.roundRect.bottomRightCornerRadius.x, - tmpOutline.roundRect.bottomRightCornerRadius.y, - tmpOutline.roundRect.bottomLeftCornerRadius.x, - tmpOutline.roundRect.bottomLeftCornerRadius.y - ), - antiAlias = true - ) - is Outline.Generic -> renderNode.setClipPath(tmpOutline.path.materializeSkiaPath(), antiAlias = true) - } - } - outlineDirty = false - } - - private inline fun resolveOutlinePosition(block: (Offset, Size) -> T): T { - val layerSize = this.size.toSize() - val rRectTopLeft = roundRectOutlineTopLeft - val rRectSize = roundRectOutlineSize - - val outlineSize = - if (rRectSize.isUnspecified) { - layerSize - } else { - rRectSize - } - return block(rRectTopLeft, outlineSize) - } - - internal fun release() { - if (!isReleased) { - isReleased = true - discardContentIfReleasedAndHaveNoParentLayerUsages() - } - } - - private fun discardContentIfReleasedAndHaveNoParentLayerUsages() { - if (isReleased && parentLayerUsages == 0) { - // discarding means we don't draw children layer anymore and need to remove dependencies: - childDependenciesTracker.removeDependencies { it.onRemovedFromParentLayer() } - - renderNode?.close() - renderNode = null - } - } - - actual suspend fun toImageBitmap(): ImageBitmap = - ImageBitmap(size.width, size.height).apply { draw(Canvas(this), null) } - - private fun updateLayerProperties() { - renderNode?.layerPaint = if (requiresLayer()) { - SkPaint().also { - it.setAlphaf(alpha) - it.imageFilter = renderEffect?.skiaImageFilter - it.colorFilter = colorFilter?.asSkiaColorFilter() - it.blendMode = blendMode.toSkia() - } - } else { - null - } - } - - private fun requiresLayer(): Boolean { - val alphaNeedsLayer = alpha < 1f && compositingStrategy != CompositingStrategy.ModulateAlpha - val hasColorFilter = colorFilter != null - val hasBlendMode = blendMode != BlendMode.SrcOver - val hasRenderEffect = renderEffect != null - val offscreenBufferRequested = compositingStrategy == CompositingStrategy.Offscreen - return alphaNeedsLayer || hasColorFilter || hasBlendMode || hasRenderEffect || - offscreenBufferRequested - } - - actual fun setOutsets( - @IntRange(from = 0) left: Int, - @IntRange(from = 0) top: Int, - @IntRange(from = 0) right: Int, - @IntRange(from = 0) bottom: Int - ) { - // TODO: https://youtrack.jetbrains.com/issue/CMP-10054/Implement-GraphicsLayer.setOutsets-method - } -} diff --git a/compose/ui/ui-skiko/api/desktop/ui-skiko.api b/compose/ui/ui-skiko/api/desktop/ui-skiko.api new file mode 100644 index 0000000000000..4e0515534507f --- /dev/null +++ b/compose/ui/ui-skiko/api/desktop/ui-skiko.api @@ -0,0 +1,137 @@ +public final class androidx/compose/ui/graphics/DesktopColorFilter_desktopKt { + public static final synthetic fun asDesktopColorFilter (Landroidx/compose/ui/graphics/ColorFilter;)Lorg/jetbrains/skia/ColorFilter; + public static final synthetic fun toComposeColorFilter (Lorg/jetbrains/skia/ColorFilter;)Landroidx/compose/ui/graphics/ColorFilter; +} + +public final class androidx/compose/ui/graphics/DesktopImageAsset_desktopKt { + public static final synthetic fun asDesktopBitmap (Landroidx/compose/ui/graphics/ImageBitmap;)Lorg/jetbrains/skia/Bitmap; + public static final synthetic fun asImageBitmap (Lorg/jetbrains/skia/Bitmap;)Landroidx/compose/ui/graphics/ImageBitmap; + public static final synthetic fun asImageBitmap (Lorg/jetbrains/skia/Image;)Landroidx/compose/ui/graphics/ImageBitmap; +} + +public final class androidx/compose/ui/graphics/DesktopImageConverters_desktopKt { + public static final synthetic fun asAwtImage (Landroidx/compose/ui/graphics/ImageBitmap;)Ljava/awt/image/BufferedImage; + public static final synthetic fun asAwtImage-Ug5Nnss (Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;J)Ljava/awt/Image; + public static synthetic fun asAwtImage-Ug5Nnss$default (Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;JILjava/lang/Object;)Ljava/awt/Image; + public static final synthetic fun asPainter (Ljava/awt/image/BufferedImage;)Landroidx/compose/ui/graphics/painter/Painter; + public static final fun toAwtImage (Landroidx/compose/ui/graphics/ImageBitmap;)Ljava/awt/image/BufferedImage; + public static final fun toAwtImage-Ug5Nnss (Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;J)Ljava/awt/Image; + public static synthetic fun toAwtImage-Ug5Nnss$default (Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;JILjava/lang/Object;)Ljava/awt/Image; + public static final fun toComposeBitmap (Ljava/awt/image/BufferedImage;)Landroidx/compose/ui/graphics/ImageBitmap; + public static final fun toComposeImageBitmap (Ljava/awt/image/BufferedImage;)Landroidx/compose/ui/graphics/ImageBitmap; + public static final fun toPainter (Ljava/awt/image/BufferedImage;)Landroidx/compose/ui/graphics/painter/Painter; +} + +public final class androidx/compose/ui/graphics/DesktopPathEffect_desktopKt { + public static final synthetic fun asDesktopPathEffect (Landroidx/compose/ui/graphics/PathEffect;)Lorg/jetbrains/skia/PathEffect; +} + +public final class androidx/compose/ui/graphics/DesktopPath_desktopKt { + public static final synthetic fun asDesktopPath (Landroidx/compose/ui/graphics/Path;)Lorg/jetbrains/skia/Path; +} + +public final class androidx/compose/ui/graphics/Rects_nonAndroidKt { + public static final fun toComposeRect (Lorg/jetbrains/skia/Rect;)Landroidx/compose/ui/geometry/Rect; + public static final fun toSkiaRRect (Landroidx/compose/ui/geometry/RoundRect;)Lorg/jetbrains/skia/RRect; + public static final fun toSkiaRect (Landroidx/compose/ui/geometry/Rect;)Lorg/jetbrains/skia/Rect; +} + +public final class androidx/compose/ui/graphics/SkiaBackedCanvas_skikoKt { + public static final fun asComposeCanvas (Lorg/jetbrains/skia/Canvas;)Landroidx/compose/ui/graphics/Canvas; + public static final fun getNativeCanvas (Landroidx/compose/ui/graphics/Canvas;)Lorg/jetbrains/skia/Canvas; + public static final fun getSkiaCanvas (Landroidx/compose/ui/graphics/Canvas;)Lorg/jetbrains/skia/Canvas; +} + +public final class androidx/compose/ui/graphics/SkiaBackedPaint_skikoKt { + public static final synthetic fun Paint ()Landroidx/compose/ui/graphics/Paint; + public static final fun asComposePaint (Lorg/jetbrains/skia/Paint;)Landroidx/compose/ui/graphics/Paint; + public static final fun getSkiaPaint (Landroidx/compose/ui/graphics/Paint;)Lorg/jetbrains/skia/Paint; + public static final synthetic fun isSupported-s9anfk8 (I)Z +} + +public final class androidx/compose/ui/graphics/SkiaBackedPathEffect_skikoKt { + public static final fun asComposePathEffect (Lorg/jetbrains/skia/PathEffect;)Landroidx/compose/ui/graphics/PathEffect; + public static final fun asSkiaPathEffect (Landroidx/compose/ui/graphics/PathEffect;)Lorg/jetbrains/skia/PathEffect; +} + +public final class androidx/compose/ui/graphics/SkiaBackedPathMeasure_skikoKt { + public static final synthetic fun PathMeasure ()Landroidx/compose/ui/graphics/PathMeasure; + public static final fun asComposePathEffect (Lorg/jetbrains/skia/PathMeasure;)Landroidx/compose/ui/graphics/PathMeasure; + public static final fun asSkiaPathMeasure (Landroidx/compose/ui/graphics/PathMeasure;)Lorg/jetbrains/skia/PathMeasure; +} + +public final class androidx/compose/ui/graphics/SkiaBackedPath_skikoKt { + public static final synthetic fun Path ()Landroidx/compose/ui/graphics/Path; + public static final fun asComposePath (Lorg/jetbrains/skia/Path;)Landroidx/compose/ui/graphics/Path; + public static final fun asSkiaPath (Landroidx/compose/ui/graphics/Path;)Lorg/jetbrains/skia/Path; +} + +public final class androidx/compose/ui/graphics/SkiaBackedRenderEffect_skikoKt { + public static final fun asComposeRenderEffect (Lorg/jetbrains/skia/ImageFilter;)Landroidx/compose/ui/graphics/RenderEffect; + public static final fun getSkiaImageFilter (Landroidx/compose/ui/graphics/RenderEffect;)Lorg/jetbrains/skia/ImageFilter; +} + +public final class androidx/compose/ui/graphics/SkiaColorFilter_skikoKt { + public static final fun asComposeColorFilter (Lorg/jetbrains/skia/ColorFilter;)Landroidx/compose/ui/graphics/ColorFilter; + public static final fun asSkiaColorFilter (Landroidx/compose/ui/graphics/ColorFilter;)Lorg/jetbrains/skia/ColorFilter; +} + +public final class androidx/compose/ui/graphics/SkiaImageAsset_skikoKt { + public static final fun asComposeImageBitmap (Lorg/jetbrains/skia/Bitmap;)Landroidx/compose/ui/graphics/ImageBitmap; + public static final fun asSkiaBitmap (Landroidx/compose/ui/graphics/ImageBitmap;)Lorg/jetbrains/skia/Bitmap; + public static final fun toComposeImageBitmap (Lorg/jetbrains/skia/Image;)Landroidx/compose/ui/graphics/ImageBitmap; +} + +public final class androidx/compose/ui/graphics/SkiaPathIterator_skikoKt { + public static final synthetic fun PathIterator (Landroidx/compose/ui/graphics/Path;Landroidx/compose/ui/graphics/PathIterator$ConicEvaluation;F)Landroidx/compose/ui/graphics/PathIterator; + public static synthetic fun PathIterator$default (Landroidx/compose/ui/graphics/Path;Landroidx/compose/ui/graphics/PathIterator$ConicEvaluation;FILjava/lang/Object;)Landroidx/compose/ui/graphics/PathIterator; +} + +public final class androidx/compose/ui/graphics/SkiaShader_skikoKt { + public static final fun asComposeShader (Lorg/jetbrains/skia/Shader;)Landroidx/compose/ui/graphics/Shader; + public static final fun getSkiaShader (Landroidx/compose/ui/graphics/Shader;)Lorg/jetbrains/skia/Shader; +} + +public final class androidx/compose/ui/graphics/SkiaTileMode_skikoKt { + public static final synthetic fun isSupported-0vamqd0 (I)Z +} + +public final class androidx/compose/ui/text/platform/DesktopFont_desktopKt { + public static final fun Font-Ej4NQ78 (Ljava/io/File;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;)Landroidx/compose/ui/text/font/Font; + public static final fun Font-Ej4NQ78 (Ljava/lang/String;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;)Landroidx/compose/ui/text/font/Font; + public static synthetic fun Font-Ej4NQ78$default (Ljava/io/File;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;ILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; + public static synthetic fun Font-Ej4NQ78$default (Ljava/lang/String;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;ILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; + public static final fun Font-RetOiIg (Ljava/io/File;Landroidx/compose/ui/text/font/FontWeight;I)Landroidx/compose/ui/text/font/Font; + public static final fun Font-RetOiIg (Ljava/lang/String;Landroidx/compose/ui/text/font/FontWeight;I)Landroidx/compose/ui/text/font/Font; + public static synthetic fun Font-RetOiIg$default (Ljava/io/File;Landroidx/compose/ui/text/font/FontWeight;IILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; + public static synthetic fun Font-RetOiIg$default (Ljava/lang/String;Landroidx/compose/ui/text/font/FontWeight;IILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; +} + +public final class androidx/compose/ui/text/platform/FontLoadResult { + public static final field $stable I + public fun (Lorg/jetbrains/skia/Typeface;Ljava/util/List;)V + public final fun getAliases ()Ljava/util/List; + public final fun getTypeface ()Lorg/jetbrains/skia/Typeface; +} + +public final class androidx/compose/ui/text/platform/FontLoader : androidx/compose/ui/text/font/Font$ResourceLoader, androidx/compose/ui/text/font/FontResourceLoaderWithResolver { + public static final field $stable I + public fun ()V + public fun getFontFamilyResolver ()Landroidx/compose/ui/text/font/FontFamily$Resolver; + public synthetic fun load (Landroidx/compose/ui/text/font/Font;)Ljava/lang/Object; + public fun load (Landroidx/compose/ui/text/font/Font;)Lorg/jetbrains/skia/Typeface; +} + +public final class androidx/compose/ui/text/platform/PlatformFont_skikoKt { + public static final fun Font-MuC2MFs (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;)Landroidx/compose/ui/text/font/Font; + public static final fun Font-MuC2MFs (Ljava/lang/String;[BLandroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;)Landroidx/compose/ui/text/font/Font; + public static synthetic fun Font-MuC2MFs$default (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;ILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; + public static synthetic fun Font-MuC2MFs$default (Ljava/lang/String;[BLandroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;ILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; + public static final fun Font-wCLgNak (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/text/font/FontWeight;I)Landroidx/compose/ui/text/font/Font; + public static final fun Font-wCLgNak (Ljava/lang/String;[BLandroidx/compose/ui/text/font/FontWeight;I)Landroidx/compose/ui/text/font/Font; + public static synthetic fun Font-wCLgNak$default (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/text/font/FontWeight;IILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; + public static synthetic fun Font-wCLgNak$default (Ljava/lang/String;[BLandroidx/compose/ui/text/font/FontWeight;IILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; + public static final fun Typeface (Lorg/jetbrains/skia/Typeface;Ljava/lang/String;)Landroidx/compose/ui/text/font/Typeface; + public static synthetic fun Typeface$default (Lorg/jetbrains/skia/Typeface;Ljava/lang/String;ILjava/lang/Object;)Landroidx/compose/ui/text/font/Typeface; +} + diff --git a/compose/ui/ui-skiko/api/ui-skiko.klib.api b/compose/ui/ui-skiko/api/ui-skiko.klib.api new file mode 100644 index 0000000000000..e9a6da8322a13 --- /dev/null +++ b/compose/ui/ui-skiko/api/ui-skiko.klib.api @@ -0,0 +1,84 @@ +// Klib ABI Dump +// Targets: [iosArm64, iosSimulatorArm64, js, macosArm64, wasmJs] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class androidx.compose.ui.text.platform/FontLoadResult { // androidx.compose.ui.text.platform/FontLoadResult|null[0] + constructor (org.jetbrains.skia/Typeface?, kotlin.collections/List) // androidx.compose.ui.text.platform/FontLoadResult.|(org.jetbrains.skia.Typeface?;kotlin.collections.List){}[0] + + final val aliases // androidx.compose.ui.text.platform/FontLoadResult.aliases|{}aliases[0] + final fun (): kotlin.collections/List // androidx.compose.ui.text.platform/FontLoadResult.aliases.|(){}[0] + final val typeface // androidx.compose.ui.text.platform/FontLoadResult.typeface|{}typeface[0] + final fun (): org.jetbrains.skia/Typeface? // androidx.compose.ui.text.platform/FontLoadResult.typeface.|(){}[0] +} + +final class androidx.compose.ui.text.platform/FontLoader : androidx.compose.ui.text.font/Font.ResourceLoader, androidx.compose.ui.text.font/FontResourceLoaderWithResolver { // androidx.compose.ui.text.platform/FontLoader|null[0] + constructor () // androidx.compose.ui.text.platform/FontLoader.|(){}[0] + + final val fontFamilyResolver // androidx.compose.ui.text.platform/FontLoader.fontFamilyResolver|{}fontFamilyResolver[0] + final fun (): androidx.compose.ui.text.font/FontFamily.Resolver // androidx.compose.ui.text.platform/FontLoader.fontFamilyResolver.|(){}[0] + + final fun load(androidx.compose.ui.text.font/Font): org.jetbrains.skia/Typeface // androidx.compose.ui.text.platform/FontLoader.load|load(androidx.compose.ui.text.font.Font){}[0] +} + +final val androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop // androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop|#static{}androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop[0] +final val androidx.compose.ui.graphics/nativeCanvas // androidx.compose.ui.graphics/nativeCanvas|@androidx.compose.ui.graphics.Canvas{}nativeCanvas[0] + final fun (androidx.compose.ui.graphics/Canvas).(): org.jetbrains.skia/Canvas // androidx.compose.ui.graphics/nativeCanvas.|@androidx.compose.ui.graphics.Canvas(){}[0] +final val androidx.compose.ui.graphics/skiaCanvas // androidx.compose.ui.graphics/skiaCanvas|@androidx.compose.ui.graphics.Canvas{}skiaCanvas[0] + final fun (androidx.compose.ui.graphics/Canvas).(): org.jetbrains.skia/Canvas // androidx.compose.ui.graphics/skiaCanvas.|@androidx.compose.ui.graphics.Canvas(){}[0] +final val androidx.compose.ui.graphics/skiaImageFilter // androidx.compose.ui.graphics/skiaImageFilter|@androidx.compose.ui.graphics.RenderEffect{}skiaImageFilter[0] + final fun (androidx.compose.ui.graphics/RenderEffect).(): org.jetbrains.skia/ImageFilter // androidx.compose.ui.graphics/skiaImageFilter.|@androidx.compose.ui.graphics.RenderEffect(){}[0] +final val androidx.compose.ui.graphics/skiaPaint // androidx.compose.ui.graphics/skiaPaint|@androidx.compose.ui.graphics.Paint{}skiaPaint[0] + final fun (androidx.compose.ui.graphics/Paint).(): org.jetbrains.skia/Paint // androidx.compose.ui.graphics/skiaPaint.|@androidx.compose.ui.graphics.Paint(){}[0] +final val androidx.compose.ui.graphics/skiaShader // androidx.compose.ui.graphics/skiaShader|@androidx.compose.ui.graphics.Shader{}skiaShader[0] + final fun (androidx.compose.ui.graphics/Shader).(): org.jetbrains.skia/Shader // androidx.compose.ui.graphics/skiaShader.|@androidx.compose.ui.graphics.Shader(){}[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop|#static{}androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop|#static{}androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoadResult$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoadResult$stableprop|#static{}androidx_compose_ui_text_platform_FontLoadResult$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoader$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoader$stableprop|#static{}androidx_compose_ui_text_platform_FontLoader$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop[0] + +final fun (androidx.compose.ui.geometry/Rect).androidx.compose.ui.graphics/toSkiaRect(): org.jetbrains.skia/Rect // androidx.compose.ui.graphics/toSkiaRect|toSkiaRect@androidx.compose.ui.geometry.Rect(){}[0] +final fun (androidx.compose.ui.geometry/RoundRect).androidx.compose.ui.graphics/toSkiaRRect(): org.jetbrains.skia/RRect // androidx.compose.ui.graphics/toSkiaRRect|toSkiaRRect@androidx.compose.ui.geometry.RoundRect(){}[0] +final fun (androidx.compose.ui.graphics/ColorFilter).androidx.compose.ui.graphics/asSkiaColorFilter(): org.jetbrains.skia/ColorFilter // androidx.compose.ui.graphics/asSkiaColorFilter|asSkiaColorFilter@androidx.compose.ui.graphics.ColorFilter(){}[0] +final fun (androidx.compose.ui.graphics/ImageBitmap).androidx.compose.ui.graphics/asSkiaBitmap(): org.jetbrains.skia/Bitmap // androidx.compose.ui.graphics/asSkiaBitmap|asSkiaBitmap@androidx.compose.ui.graphics.ImageBitmap(){}[0] +final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/asSkiaPath(): org.jetbrains.skia/Path // androidx.compose.ui.graphics/asSkiaPath|asSkiaPath@androidx.compose.ui.graphics.Path(){}[0] +final fun (androidx.compose.ui.graphics/PathEffect).androidx.compose.ui.graphics/asSkiaPathEffect(): org.jetbrains.skia/PathEffect // androidx.compose.ui.graphics/asSkiaPathEffect|asSkiaPathEffect@androidx.compose.ui.graphics.PathEffect(){}[0] +final fun (androidx.compose.ui.graphics/PathMeasure).androidx.compose.ui.graphics/asSkiaPathMeasure(): org.jetbrains.skia/PathMeasure // androidx.compose.ui.graphics/asSkiaPathMeasure|asSkiaPathMeasure@androidx.compose.ui.graphics.PathMeasure(){}[0] +final fun (org.jetbrains.skia/Bitmap).androidx.compose.ui.graphics/asComposeImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // androidx.compose.ui.graphics/asComposeImageBitmap|asComposeImageBitmap@org.jetbrains.skia.Bitmap(){}[0] +final fun (org.jetbrains.skia/Canvas).androidx.compose.ui.graphics/asComposeCanvas(): androidx.compose.ui.graphics/Canvas // androidx.compose.ui.graphics/asComposeCanvas|asComposeCanvas@org.jetbrains.skia.Canvas(){}[0] +final fun (org.jetbrains.skia/ColorFilter).androidx.compose.ui.graphics/asComposeColorFilter(): androidx.compose.ui.graphics/ColorFilter // androidx.compose.ui.graphics/asComposeColorFilter|asComposeColorFilter@org.jetbrains.skia.ColorFilter(){}[0] +final fun (org.jetbrains.skia/Image).androidx.compose.ui.graphics/toComposeImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // androidx.compose.ui.graphics/toComposeImageBitmap|toComposeImageBitmap@org.jetbrains.skia.Image(){}[0] +final fun (org.jetbrains.skia/ImageFilter).androidx.compose.ui.graphics/asComposeRenderEffect(): androidx.compose.ui.graphics/RenderEffect // androidx.compose.ui.graphics/asComposeRenderEffect|asComposeRenderEffect@org.jetbrains.skia.ImageFilter(){}[0] +final fun (org.jetbrains.skia/Paint).androidx.compose.ui.graphics/asComposePaint(): androidx.compose.ui.graphics/Paint // androidx.compose.ui.graphics/asComposePaint|asComposePaint@org.jetbrains.skia.Paint(){}[0] +final fun (org.jetbrains.skia/Path).androidx.compose.ui.graphics/asComposePath(): androidx.compose.ui.graphics/Path // androidx.compose.ui.graphics/asComposePath|asComposePath@org.jetbrains.skia.Path(){}[0] +final fun (org.jetbrains.skia/PathEffect).androidx.compose.ui.graphics/asComposePathEffect(): androidx.compose.ui.graphics/PathEffect // androidx.compose.ui.graphics/asComposePathEffect|asComposePathEffect@org.jetbrains.skia.PathEffect(){}[0] +final fun (org.jetbrains.skia/PathMeasure).androidx.compose.ui.graphics/asComposePathEffect(): androidx.compose.ui.graphics/PathMeasure // androidx.compose.ui.graphics/asComposePathEffect|asComposePathEffect@org.jetbrains.skia.PathMeasure(){}[0] +final fun (org.jetbrains.skia/Rect).androidx.compose.ui.graphics/toComposeRect(): androidx.compose.ui.geometry/Rect // androidx.compose.ui.graphics/toComposeRect|toComposeRect@org.jetbrains.skia.Rect(){}[0] +final fun (org.jetbrains.skia/Shader).androidx.compose.ui.graphics/asComposeShader(): androidx.compose.ui.graphics/Shader // androidx.compose.ui.graphics/asComposeShader|asComposeShader@org.jetbrains.skia.Shader(){}[0] +final fun androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop_getter(): kotlin/Int // androidx.compose.ui.graphics/androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop_getter|androidx_compose_ui_graphics_SkiaGraphicsContext$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/Font(kotlin/String, kotlin/ByteArray, androidx.compose.ui.text.font/FontWeight = ..., androidx.compose.ui.text.font/FontStyle = ...): androidx.compose.ui.text.font/Font // androidx.compose.ui.text.platform/Font|Font(kotlin.String;kotlin.ByteArray;androidx.compose.ui.text.font.FontWeight;androidx.compose.ui.text.font.FontStyle){}[0] +final fun androidx.compose.ui.text.platform/Font(kotlin/String, kotlin/ByteArray, androidx.compose.ui.text.font/FontWeight = ..., androidx.compose.ui.text.font/FontStyle = ..., androidx.compose.ui.text.font/FontVariation.Settings = ...): androidx.compose.ui.text.font/Font // androidx.compose.ui.text.platform/Font|Font(kotlin.String;kotlin.ByteArray;androidx.compose.ui.text.font.FontWeight;androidx.compose.ui.text.font.FontStyle;androidx.compose.ui.text.font.FontVariation.Settings){}[0] +final fun androidx.compose.ui.text.platform/Font(kotlin/String, kotlin/Function0, androidx.compose.ui.text.font/FontWeight = ..., androidx.compose.ui.text.font/FontStyle = ...): androidx.compose.ui.text.font/Font // androidx.compose.ui.text.platform/Font|Font(kotlin.String;kotlin.Function0;androidx.compose.ui.text.font.FontWeight;androidx.compose.ui.text.font.FontStyle){}[0] +final fun androidx.compose.ui.text.platform/Font(kotlin/String, kotlin/Function0, androidx.compose.ui.text.font/FontWeight = ..., androidx.compose.ui.text.font/FontStyle = ..., androidx.compose.ui.text.font/FontVariation.Settings = ...): androidx.compose.ui.text.font/Font // androidx.compose.ui.text.platform/Font|Font(kotlin.String;kotlin.Function0;androidx.compose.ui.text.font.FontWeight;androidx.compose.ui.text.font.FontStyle;androidx.compose.ui.text.font.FontVariation.Settings){}[0] +final fun androidx.compose.ui.text.platform/Typeface(org.jetbrains.skia/Typeface, kotlin/String? = ...): androidx.compose.ui.text.font/Typeface // androidx.compose.ui.text.platform/Typeface|Typeface(org.jetbrains.skia.Typeface;kotlin.String?){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop_getter|androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop_getter|androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoadResult$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoadResult$stableprop_getter|androidx_compose_ui_text_platform_FontLoadResult$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoader$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoader$stableprop_getter|androidx_compose_ui_text_platform_FontLoader$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop_getter(){}[0] diff --git a/compose/ui/ui-skiko/build.gradle b/compose/ui/ui-skiko/build.gradle new file mode 100644 index 0000000000000..9c060055f3324 --- /dev/null +++ b/compose/ui/ui-skiko/build.gradle @@ -0,0 +1,113 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.build.SoftwareType +import androidx.build.PlatformIdentifier + +plugins { + id("AndroidXPlugin") + id("AndroidXComposePlugin") + id("JetBrainsAndroidXPlugin") +} + +androidXMultiplatform { + androidLibrary { + compileSdk = 35 + namespace = "androidx.compose.ui.skiko" + } + desktop() + mac() + ios() + js() + wasmJs() + + defaultPlatform(PlatformIdentifier.ANDROID) + + sourceSets { + commonMain.dependencies { + api(project(":compose:ui:ui-graphics")) + api(project(":compose:ui:ui-text")) + implementation(project(":compose:runtime:runtime")) + implementation(project(":compose:ui:ui-util")) + } + + commonTest.dependencies { + implementation(libs.kotlinTest) + } + + nonAndroidMain { + dependsOn(commonMain) + dependencies { + implementation(libs.atomicFu) + api(libs.skiko) + } + } + + nonAndroidTest { + dependsOn(commonTest) + dependencies { + implementation(project(":kruth:kruth")) + implementation(project(":compose:ui:ui-test")) + } + } + + nonAndroidExcludingWebMain { + dependsOn(nonAndroidMain) + } + + desktopMain { + dependsOn(nonAndroidMain) + dependsOn(nonAndroidExcludingWebMain) + } + + desktopTest { + dependsOn(nonAndroidTest) + dependencies { + implementation(libs.skikoCurrentOs) + implementation(libs.junit) + implementation(libs.truth) + implementation(project(":internal-testutils-fonts")) + implementation(project(":compose:foundation:foundation")) + implementation(project(":compose:ui:ui-test-junit4")) + } + } + + nonJvmMain { + dependsOn(nonAndroidMain) + } + + nonJvmTest { + dependsOn(nonAndroidTest) + } + + nativeMain { + dependsOn(nonJvmMain) + dependsOn(nonAndroidExcludingWebMain) + } + + nativeTest { + dependsOn(nonJvmTest) + } + } +} + +androidx { + name = "Compose UI Skiko" + type = SoftwareType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS + inceptionYear = "2026" + description = "Skiko-backed runtime implementations for Compose UI" + legacyDisableKotlinStrictApiMode = true +} diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.skiko.kt b/compose/ui/ui-skiko/src/commonTest/kotlin/kotlinx/test/IgnoreTargets.kt similarity index 69% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.skiko.kt rename to compose/ui/ui-skiko/src/commonTest/kotlin/kotlinx/test/IgnoreTargets.kt index 3c3d5944597d0..49cfe21c13b2f 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.skiko.kt +++ b/compose/ui/ui-skiko/src/commonTest/kotlin/kotlinx/test/IgnoreTargets.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Android Open Source Project + * Copyright 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,12 @@ * limitations under the License. */ -package androidx.compose.ui.graphics +@file:OptIn(ExperimentalMultiplatform::class) -import org.jetbrains.skia.Image +package kotlinx.test -internal actual fun createImageBitmap(bytes: ByteArray): ImageBitmap = - Image.makeFromEncoded(bytes).toComposeImageBitmap() +@OptionalExpectation +expect annotation class IgnoreWasmTarget() + +@OptionalExpectation +expect annotation class IgnoreJsTarget() diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt similarity index 88% rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt index 535498610a306..bf278c4aaade5 100644 --- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt @@ -26,7 +26,7 @@ import org.jetbrains.skia.ColorFilter as SkColorFilter replaceWith = ReplaceWith("asSkiaColorFilter()"), level = DeprecationLevel.HIDDEN, ) -fun ColorFilter.asDesktopColorFilter(): SkColorFilter = nativeColorFilter +fun ColorFilter.asDesktopColorFilter(): SkColorFilter = asSkiaColorFilter() /** * Obtain a [org.jetbrains.skia.ColorFilter] instance from this [ColorFilter] @@ -36,4 +36,4 @@ fun ColorFilter.asDesktopColorFilter(): SkColorFilter = nativeColorFilter replaceWith = ReplaceWith("asComposeColorFilter()"), level = DeprecationLevel.HIDDEN, ) -fun SkColorFilter.toComposeColorFilter(): ColorFilter = ColorFilter(this) +fun SkColorFilter.toComposeColorFilter(): ColorFilter = asComposeColorFilter() diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageAsset.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageAsset.desktop.kt similarity index 100% rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageAsset.desktop.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageAsset.desktop.kt diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageConverters.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageConverters.desktop.kt similarity index 100% rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageConverters.desktop.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageConverters.desktop.kt diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt similarity index 89% rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt index 9c9eb2284d1e2..9c859363924f6 100644 --- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt @@ -16,10 +16,12 @@ package androidx.compose.ui.graphics +import org.jetbrains.skia.Path as SkPath + @Suppress("NOTHING_TO_INLINE") @Deprecated( message = "Use asSkiaPath()", replaceWith = ReplaceWith("asSkiaPath()"), level = DeprecationLevel.HIDDEN, ) -inline fun Path.asDesktopPath(): org.jetbrains.skia.Path = asSkiaPath() +inline fun Path.asDesktopPath(): SkPath = asSkiaPath() diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.desktop.kt similarity index 100% rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.desktop.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.desktop.kt diff --git a/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaintCompat.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaintCompat.desktop.kt new file mode 100644 index 0000000000000..e4011df03699c --- /dev/null +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaintCompat.desktop.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("SkiaBackedPaint_skikoKt") +@file:JvmMultifileClass + +package androidx.compose.ui.graphics + +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +@Deprecated( + "Binary-compatibility shim for the relocated Paint() factory.", + level = DeprecationLevel.HIDDEN, +) +@JvmName("Paint") +fun paintFactoryBinaryCompatShim(): Paint = Paint() + +@Deprecated( + "Binary-compatibility shim for the relocated BlendMode.isSupported().", + level = DeprecationLevel.HIDDEN, +) +@JvmName("isSupported-s9anfk8") +fun blendModeIsSupportedBinaryCompatShim(blendMode: BlendMode): Boolean = blendMode.isSupported() diff --git a/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathCompat.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathCompat.desktop.kt new file mode 100644 index 0000000000000..a8b401224b58c --- /dev/null +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathCompat.desktop.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("SkiaBackedPath_skikoKt") +@file:JvmMultifileClass + +package androidx.compose.ui.graphics + +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +@Deprecated( + "Binary-compatibility shim for the relocated Path() factory.", + level = DeprecationLevel.HIDDEN, +) +@JvmName("Path") +fun pathFactoryBinaryCompatShim(): Path = Path() diff --git a/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasureCompat.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasureCompat.desktop.kt new file mode 100644 index 0000000000000..54eda7cd91e8c --- /dev/null +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasureCompat.desktop.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("SkiaBackedPathMeasure_skikoKt") +@file:JvmMultifileClass + +package androidx.compose.ui.graphics + +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +@Deprecated( + "Binary-compatibility shim for the relocated PathMeasure() factory.", + level = DeprecationLevel.HIDDEN, +) +@JvmName("PathMeasure") +fun pathMeasureFactoryBinaryCompatShim(): PathMeasure = PathMeasure() diff --git a/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaPathIteratorCompat.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaPathIteratorCompat.desktop.kt new file mode 100644 index 0000000000000..32e85e03abab9 --- /dev/null +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaPathIteratorCompat.desktop.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("SkiaPathIterator_skikoKt") + +package androidx.compose.ui.graphics + +import kotlin.jvm.JvmName + +@Deprecated( + "Binary-compatibility shim for the relocated PathIterator() factory.", + level = DeprecationLevel.HIDDEN, +) +@JvmName("PathIterator") +fun pathIteratorBinaryCompatShim( + path: Path, + conicEvaluation: PathIterator.ConicEvaluation = PathIterator.ConicEvaluation.AsQuadratics, + tolerance: Float = 0.25f, +): PathIterator = PathIterator(path, conicEvaluation, tolerance) diff --git a/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaTileModeCompat.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaTileModeCompat.desktop.kt new file mode 100644 index 0000000000000..04f1c97bd0bf2 --- /dev/null +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/graphics/SkiaTileModeCompat.desktop.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("SkiaTileMode_skikoKt") + +package androidx.compose.ui.graphics + +import kotlin.jvm.JvmName + +@Deprecated( + "Binary-compatibility shim for the relocated TileMode.isSupported().", + level = DeprecationLevel.HIDDEN, +) +@JvmName("isSupported-0vamqd0") +fun tileModeIsSupportedBinaryCompatShim(tileMode: TileMode): Boolean = tileMode.isSupported() diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/Cache.jvm.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/Cache.jvm.kt similarity index 100% rename from compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/Cache.jvm.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/Cache.jvm.kt diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/CharHelpers.jvm.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/CharHelpers.jvm.kt similarity index 100% rename from compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/CharHelpers.jvm.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/CharHelpers.jvm.kt index a518dec096c5a..dae7baef3ad84 100644 --- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/CharHelpers.jvm.kt +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/CharHelpers.jvm.kt @@ -15,7 +15,6 @@ */ package androidx.compose.ui.text - /** * Get strong (R, L or AL) direction type. * See https://www.unicode.org/reports/tr9/ @@ -29,6 +28,7 @@ internal actual fun CodePoint.strongDirectionType(): StrongDirectionType = else -> StrongDirectionType.None } + internal actual fun CodePoint.isNeutralDirection(): Boolean = when (getDirectionality()) { CharDirectionality.OTHER_NEUTRALS, diff --git a/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/font/EmbeddedFontFamily.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/font/EmbeddedFontFamily.desktop.kt new file mode 100644 index 0000000000000..d71168c392f26 --- /dev/null +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/font/EmbeddedFontFamily.desktop.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.text.font + +import androidx.compose.runtime.Stable +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.platform.JetBrainsRuntimeFontFamilies + +/** + * Load a [FontFamily] embedded in the JetBrains Runtime. It will return + * `null` if the font family isn't embedded in the JetBrains Runtime, + * or if using any Java runtime other than the JetBrains Runtime. + * + * Using this requires the current module to have access to the `sun.font` + * APIs, which are closed by default. To open the access to those APIs, you + * need to pass this argument to the JVM: + * ``` + * --add-opens java.desktop/sun.font=ALL-UNNAMED + * ``` + * + * If the `sun.font` API is not accessible, this will always return `null`. + * + * @param familyName The case-insensitive font family name to load. + * @return the requested font family, if running on the JetBrains Runtime, + * and the `sun.font` API is accessible, and the font family exists in + * the runtime. Otherwise, `null`. + */ +@ExperimentalTextApi +@Stable +fun EmbeddedFontFamily(familyName: String): FontFamily? = + JetBrainsRuntimeFontFamilies.embeddedFamilies[familyName.lowercase()] diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/AwtFontInterop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/AwtFontInterop.kt similarity index 100% rename from compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/AwtFontInterop.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/AwtFontInterop.kt diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/AwtFontUtils.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/AwtFontUtils.kt similarity index 100% rename from compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/AwtFontUtils.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/AwtFontUtils.kt diff --git a/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.desktop.kt new file mode 100644 index 0000000000000..b501320ee8bd4 --- /dev/null +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.desktop.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.text.platform + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontVariation +import androidx.compose.ui.text.font.FontWeight +import java.io.File +import org.jetbrains.skia.Data +import org.jetbrains.skia.FontMgr +import org.jetbrains.skia.FontSlant +import org.jetbrains.skia.FontStyle as SkFontStyle +import org.jetbrains.skia.FontWidth +import org.jetbrains.skia.Typeface as SkTypeface + +@OptIn(ExperimentalTextApi::class) +internal actual fun loadTypeface(font: Font): SkTypeface { + if (font !is PlatformFont) { + throw IllegalArgumentException("Unsupported font type: $font") + } + val typeface = when (font) { + is ResourceFont -> typefaceResource(font.name) + is FileFont -> FontMgr.default.makeFromFile(font.file.toString()) + is LoadedFont -> FontMgr.default.makeFromData(Data.makeFromBytes(font.data)) + is SystemFont -> FontMgr.default.matchFamilyStyle(font.identity, font.skFontStyle) + } ?: (FontMgr.default.legacyMakeTypeface(font.identity, font.skFontStyle) + ?: error("loadTypeface legacyMakeTypeface failed")) + return typeface.cloneWithVariationSettings(font.variationSettings) +} + +private fun typefaceResource(resourceName: String): SkTypeface { + val contextClassLoader = Thread.currentThread().contextClassLoader!! + val resource = contextClassLoader.getResourceAsStream(resourceName) + ?: (::typefaceResource.javaClass).getResourceAsStream(resourceName) + ?: error("Can't load font from $resourceName") + + val bytes = resource.use { it.readAllBytes() } + return FontMgr.default.makeFromData(Data.makeFromBytes(bytes))!! +} + +private val Font.skFontStyle: SkFontStyle + get() = SkFontStyle( + weight = weight.weight, + width = FontWidth.NORMAL, + slant = if (style == FontStyle.Italic) FontSlant.ITALIC else FontSlant.UPRIGHT + ) + +internal actual fun currentPlatform(): Platform { + val name = System.getProperty("os.name") + return when { + name.startsWith("Linux") -> Platform.Linux + name.startsWith("Win") -> Platform.Windows + name == "Mac OS X" -> Platform.MacOS + else -> Platform.Unknown + } +} + +/** + * Creates a Font using a resource name. + * + * @param resource The resource name in classpath. + * @param weight The weight of the font. The system uses this to match a font to a font request that + * is given in a [androidx.compose.ui.text.SpanStyle]. + * @param style The style of the font, normal or italic. The system uses this to match a font to a + * font request that is given in a [androidx.compose.ui.text.SpanStyle]. + * @see FontFamily + */ +@OptIn(InternalComposeUiApi::class) +fun Font( + resource: String, + weight: FontWeight = FontWeight.Normal, + style: FontStyle = FontStyle.Normal +): Font = ResourceFont(resource, weight, style, FontVariation.Settings()) + +@OptIn(InternalComposeUiApi::class) +fun Font( + resource: String, + weight: FontWeight = FontWeight.Normal, + style: FontStyle = FontStyle.Normal, + variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style) +): Font = ResourceFont(resource, weight, style, variationSettings) + +/** + * Creates a Font using a file path. + * + * @param file File path to font. + * @param weight The weight of the font. The system uses this to match a font to a font request that + * is given in a [androidx.compose.ui.text.SpanStyle]. + * @param style The style of the font, normal or italic. The system uses this to match a font to a + * font request that is given in a [androidx.compose.ui.text.SpanStyle]. + * @see FontFamily + */ +@OptIn(InternalComposeUiApi::class) +fun Font( + file: File, + weight: FontWeight = FontWeight.Normal, + style: FontStyle = FontStyle.Normal +): Font = FileFont(file, weight, style, FontVariation.Settings()) + +@OptIn(InternalComposeUiApi::class) +fun Font( + file: File, + weight: FontWeight = FontWeight.Normal, + style: FontStyle = FontStyle.Normal, + variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style) +): Font = FileFont(file, weight, style, variationSettings) diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/InternalFontApiChecker.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/InternalFontApiChecker.kt similarity index 100% rename from compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/InternalFontApiChecker.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/InternalFontApiChecker.kt diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/JetBrainsRuntimeFontFamilies.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/JetBrainsRuntimeFontFamilies.kt similarity index 97% rename from compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/JetBrainsRuntimeFontFamilies.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/JetBrainsRuntimeFontFamilies.kt index 5cd9f04d5d406..9ddd5e3e26a57 100644 --- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/JetBrainsRuntimeFontFamilies.kt +++ b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/JetBrainsRuntimeFontFamilies.kt @@ -17,7 +17,6 @@ package androidx.compose.ui.text.platform import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontListFontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.util.fastDistinctBy @@ -112,7 +111,7 @@ internal object JetBrainsRuntimeFontFamilies { .distinctBy { (_, font) -> font.file.absolutePath } .groupBy { (familyName, _) -> familyName } .forEach { (identity, fileFonts) -> - val fontFamily = FontListFontFamily(fileFonts.fastMap { it.second }) + val fontFamily = FontFamily(fileFonts.fastMap { it.second }) embeddedFamilies += identity.lowercase() to fontFamily } } finally { diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.desktop.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.desktop.kt similarity index 100% rename from compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.desktop.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.desktop.kt diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/ReflectionUtil.kt b/compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/ReflectionUtil.kt similarity index 100% rename from compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/ReflectionUtil.kt rename to compose/ui/ui-skiko/src/desktopMain/kotlin/androidx/compose/ui/text/platform/ReflectionUtil.kt diff --git a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopFontTest.kt b/compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopFontTest.kt similarity index 66% rename from compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopFontTest.kt rename to compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopFontTest.kt index 88c56c1261d40..00cccd4590648 100644 --- a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopFontTest.kt +++ b/compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopFontTest.kt @@ -16,39 +16,21 @@ package androidx.compose.ui.text -import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.SkiaFontLoader -import androidx.compose.ui.text.platform.Font +import androidx.compose.ui.text.platform.FontCache import androidx.compose.ui.text.platform.Typeface import androidx.compose.ui.text.platform.aliases import com.google.common.truth.Truth import org.jetbrains.skia.Data import org.jetbrains.skia.FontMgr -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class DesktopFontTest { - @get:Rule - val rule = createComposeRule() - private val fontLoader = SkiaFontLoader() - - private val fontListFontFamily by lazy { - FontFamily( - Font( - "font_desktop/sample_font.ttf" - ), - Font( - "font/test_400_italic.ttf", - style = FontStyle.Italic - ) - ) - } + private val fontCache = FontCache() private val loadedTypeface by lazy { val bytes = Thread @@ -66,13 +48,13 @@ class DesktopFontTest { @Test fun ensureRegistered() { - Truth.assertThat(fontLoader.loadPlatformTypes(FontFamily.Cursive).aliases) + Truth.assertThat(fontCache.loadPlatformTypes(FontFamily.Cursive).aliases) .isEqualTo(FontFamily.Cursive.aliases) - Truth.assertThat(fontLoader.loadPlatformTypes(FontFamily.Default).aliases) + Truth.assertThat(fontCache.loadPlatformTypes(FontFamily.Default).aliases) .isEqualTo(FontFamily.SansSerif.aliases) - Truth.assertThat(fontLoader.loadPlatformTypes(loadedFontFamily).aliases) + Truth.assertThat(fontCache.loadPlatformTypes(loadedFontFamily).aliases) .isEqualTo(listOf("Sample Font")) } -} \ No newline at end of file +} diff --git a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphIntegrationLineHeightStyleTest.kt b/compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphIntegrationLineHeightStyleTest.kt similarity index 97% rename from compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphIntegrationLineHeightStyleTest.kt rename to compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphIntegrationLineHeightStyleTest.kt index 6df4bfd1c40f2..b52592d76f15e 100644 --- a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphIntegrationLineHeightStyleTest.kt +++ b/compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphIntegrationLineHeightStyleTest.kt @@ -16,11 +16,14 @@ package androidx.compose.ui.text +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.text.platform.Font +import androidx.compose.ui.text.PlatformParagraph import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.LineHeightStyle.Alignment import androidx.compose.ui.text.style.LineHeightStyle.Trim @@ -33,13 +36,28 @@ import kotlin.math.abs import kotlin.math.ceil import kotlin.math.roundToInt import org.jetbrains.skia.FontMetrics +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @RunWith(JUnit4::class) +@OptIn(InternalComposeUiApi::class) class DesktopParagraphIntegrationLineHeightStyleTest { - private val fontFamilyResolver = createFontFamilyResolver() + @Before + fun setup() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once async-registration tests (AWT) no longer rely on the + // backend registration persisting across tests. + // @After + // fun cleanup() { + // clearSkikoComposeImplementation() + // } + + // Lazy so the registry is populated by [setup] before the resolver is created. + private val fontFamilyResolver by lazy { createFontFamilyResolver() } private val fontFamilyMeasureFont = FontFamily( Font( @@ -868,12 +886,12 @@ class DesktopParagraphIntegrationLineHeightStyleTest { val paragraphWithEmptyLastLine = simpleParagraph( text = textWithEmptyLine, style = textStyle - ) as SkiaParagraph + ) as PlatformParagraph val otherParagraph = simpleParagraph( text = textWithoutEmptyLine, style = textStyle - ) as SkiaParagraph + ) as PlatformParagraph with(paragraphWithEmptyLastLine) { for (line in 0 until lineCount) { @@ -892,7 +910,7 @@ class DesktopParagraphIntegrationLineHeightStyleTest { lineHeightTrim: Trim, lineHeightAlignment: Alignment, text: String = "AAA" - ): SkiaParagraph { + ): PlatformParagraph { val textStyle = TextStyle( lineHeightStyle = LineHeightStyle( trim = lineHeightTrim, @@ -904,7 +922,7 @@ class DesktopParagraphIntegrationLineHeightStyleTest { text = text, style = textStyle, width = text.length * fontSizeInPx - ) as SkiaParagraph + ) as PlatformParagraph assertThat(paragraph.lineCount).isEqualTo(1) @@ -914,7 +932,7 @@ class DesktopParagraphIntegrationLineHeightStyleTest { private fun multiLineParagraph( lineHeightTrim: Trim, lineHeightAlignment: Alignment, - ): SkiaParagraph { + ): PlatformParagraph { val lineCount = 3 val word = "AAA" val text = "AAA".repeat(lineCount) @@ -930,7 +948,7 @@ class DesktopParagraphIntegrationLineHeightStyleTest { text = text, style = textStyle, width = word.length * fontSizeInPx - ) as SkiaParagraph + ) as PlatformParagraph assertThat(paragraph.lineCount).isEqualTo(lineCount) @@ -953,14 +971,14 @@ class DesktopParagraphIntegrationLineHeightStyleTest { lineHeight = lineHeight, ).merge(style), maxLines = maxLines, - constraints = Constraints(maxWidth = width.ceilToInt()), + constraints = Constraints(maxWidth = ceil(width).toInt()), density = defaultDensity, fontFamilyResolver = fontFamilyResolver, ) } private fun defaultFontMetrics(): FontMetricsInt { - val defaultFont = (simpleParagraph() as SkiaParagraph).defaultFont + val defaultFont = (simpleParagraph() as PlatformParagraph).skiaDefaultFont return FontMetricsInt(defaultFont.metrics) } diff --git a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt b/compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt similarity index 100% rename from compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt rename to compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt diff --git a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/platform/AwtFontInteropTest.kt b/compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/platform/AwtFontInteropTest.kt similarity index 97% rename from compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/platform/AwtFontInteropTest.kt rename to compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/platform/AwtFontInteropTest.kt index 8a984248e46e1..4d1250ee86ba0 100644 --- a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/platform/AwtFontInteropTest.kt +++ b/compose/ui/ui-skiko/src/desktopTest/kotlin/androidx/compose/ui/text/platform/AwtFontInteropTest.kt @@ -18,6 +18,7 @@ package androidx.compose.ui.text.platform import androidx.compose.foundation.text.BasicText import androidx.compose.ui.renderComposeScene +import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -29,6 +30,7 @@ import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test +@OptIn(ExperimentalTextApi::class) class AwtFontInteropTest { @Before diff --git a/compose/ui/ui-graphics/src/nativeMain/kotlin/androidx/compose/ui/graphics/NativeImageAsset.native.kt b/compose/ui/ui-skiko/src/nativeMain/kotlin/androidx/compose/ui/graphics/NativeImageAsset.native.kt similarity index 97% rename from compose/ui/ui-graphics/src/nativeMain/kotlin/androidx/compose/ui/graphics/NativeImageAsset.native.kt rename to compose/ui/ui-skiko/src/nativeMain/kotlin/androidx/compose/ui/graphics/NativeImageAsset.native.kt index c931c7651bfe1..bd216007cbe5e 100644 --- a/compose/ui/ui-graphics/src/nativeMain/kotlin/androidx/compose/ui/graphics/NativeImageAsset.native.kt +++ b/compose/ui/ui-skiko/src/nativeMain/kotlin/androidx/compose/ui/graphics/NativeImageAsset.native.kt @@ -17,8 +17,8 @@ package androidx.compose.ui.graphics import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.usePinned import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned import platform.posix.memcpy @OptIn(ExperimentalForeignApi::class) @@ -26,8 +26,7 @@ internal actual fun ByteArray.putBytesInto(array: IntArray, offset: Int, length: this.usePinned { bytes -> array.usePinned { ints -> // Assuming little endian. - memcpy(ints.addressOf(offset), bytes.addressOf(0), (length*4).toULong()) + memcpy(ints.addressOf(offset), bytes.addressOf(0), (length * 4).toULong()) } } } - diff --git a/compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/Cache.native.kt b/compose/ui/ui-skiko/src/nativeMain/kotlin/androidx/compose/ui/text/Cache.native.kt similarity index 100% rename from compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/Cache.native.kt rename to compose/ui/ui-skiko/src/nativeMain/kotlin/androidx/compose/ui/text/Cache.native.kt diff --git a/compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/platform/NativeFont.native.kt b/compose/ui/ui-skiko/src/nativeMain/kotlin/androidx/compose/ui/text/platform/NativeFont.native.kt similarity index 98% rename from compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/platform/NativeFont.native.kt rename to compose/ui/ui-skiko/src/nativeMain/kotlin/androidx/compose/ui/text/platform/NativeFont.native.kt index de2f48bced99a..9db918f090c21 100644 --- a/compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/platform/NativeFont.native.kt +++ b/compose/ui/ui-skiko/src/nativeMain/kotlin/androidx/compose/ui/text/platform/NativeFont.native.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package androidx.compose.ui.text.platform import androidx.compose.ui.text.ExperimentalTextApi @@ -34,7 +35,7 @@ internal actual fun loadTypeface(font: Font): SkTypeface { } @Suppress("REDUNDANT_ELSE_IN_WHEN") return when (font) { - is LoadedFont -> FontMgr.default.makeFromData(Data.makeFromBytes(font.getData())) + is LoadedFont -> FontMgr.default.makeFromData(Data.makeFromBytes(font.data)) ?: error("loadTypeface makeFromData failed") is SystemFont -> FontMgr.default.legacyMakeTypeface(font.identity, font.skFontStyle) ?: error("loadTypeface legacyMakeTypeface failed") diff --git a/compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.native.kt b/compose/ui/ui-skiko/src/nativeMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.native.kt similarity index 100% rename from compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.native.kt rename to compose/ui/ui-skiko/src/nativeMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.native.kt diff --git a/compose/ui/ui-text/src/nativeTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt b/compose/ui/ui-skiko/src/nativeTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt similarity index 100% rename from compose/ui/ui-text/src/nativeTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt rename to compose/ui/ui-skiko/src/nativeTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt diff --git a/compose/ui/ui-graphics/src/skikoExcludingWebMain/kotlin/androidx/compose/ui/graphics/Actuals.skikoExcludingWeb.kt b/compose/ui/ui-skiko/src/nonAndroidExcludingWebMain/kotlin/androidx/compose/ui/graphics/Actuals.skikoExcludingWeb.kt similarity index 90% rename from compose/ui/ui-graphics/src/skikoExcludingWebMain/kotlin/androidx/compose/ui/graphics/Actuals.skikoExcludingWeb.kt rename to compose/ui/ui-skiko/src/nonAndroidExcludingWebMain/kotlin/androidx/compose/ui/graphics/Actuals.skikoExcludingWeb.kt index 97dc85fcd54c7..cccfd0cffb2d4 100644 --- a/compose/ui/ui-graphics/src/skikoExcludingWebMain/kotlin/androidx/compose/ui/graphics/Actuals.skikoExcludingWeb.kt +++ b/compose/ui/ui-skiko/src/nonAndroidExcludingWebMain/kotlin/androidx/compose/ui/graphics/Actuals.skikoExcludingWeb.kt @@ -17,6 +17,7 @@ package androidx.compose.ui.graphics import org.jetbrains.skia.Bitmap +import org.jetbrains.skia.Canvas as SkCanvas import org.jetbrains.skia.ColorAlphaType import org.jetbrains.skia.Image import org.jetbrains.skia.ImageInfo @@ -26,14 +27,14 @@ import org.jetbrains.skia.ImageInfo The difference becomes noticeable when running a loop of 100 calls. On Desktop/JVM: 11 ms for the current (default) implementation vs ~50ms for Bitmap.fromImage. - - The implementation for web uses `Bitmap.fromImage`, see Actuals.jsWasm.kt + + The implementation for web uses `Bitmap.fromImage`, see Actuals.web.kt. */ -internal actual fun Image.toBitmap(): Bitmap { +internal actual fun Image.toBitmap(): Bitmap { val bitmap = Bitmap() bitmap.allocPixels(ImageInfo.makeN32(width, height, ColorAlphaType.PREMUL)) - val canvas = org.jetbrains.skia.Canvas(bitmap) + val canvas = SkCanvas(bitmap) canvas.drawImage(this, 0f, 0f) bitmap.setImmutable() return bitmap -} \ No newline at end of file +} diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Rects.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Rects.nonAndroid.kt similarity index 100% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Rects.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/Rects.nonAndroid.kt diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.nonAndroid.kt similarity index 90% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.nonAndroid.kt index 58ba994e75e96..78b3070865c18 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.nonAndroid.kt @@ -14,22 +14,18 @@ * limitations under the License. */ +@file:JvmName("SkiaBackedPaint_skikoKt") +@file:JvmMultifileClass + package androidx.compose.ui.graphics +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName import org.jetbrains.skia.Paint as SkPaint import org.jetbrains.skia.PaintMode as SkPaintMode import org.jetbrains.skia.PaintStrokeCap as SkPaintStrokeCap import org.jetbrains.skia.PaintStrokeJoin as SkPaintStrokeJoin -@Deprecated( - message = "Use org.jetbrains.skia.Paint directly instead", - replaceWith = ReplaceWith("org.jetbrains.skia.Paint"), - level = DeprecationLevel.ERROR, -) -actual typealias NativePaint = SkPaint - -actual fun Paint(): Paint = SkiaBackedPaint() - /** * Convert the [org.jetbrains.skia.Paint] instance into a Compose-compatible Paint */ @@ -44,8 +40,9 @@ fun SkPaint.asComposePaint(): Paint = SkiaBackedPaint(this) */ val Paint.skiaPaint: SkPaint get() { - requirePrecondition(this is SkiaBackedPaint) { - "Extracting skia paint reference is only supported from androidx.compose.ui.graphics.SkiaBackedPaint instances but received ${this::class}" + require(this is SkiaBackedPaint) { + "Extracting the Skia paint reference is only supported from " + + "androidx.compose.ui.graphics.SkiaBackedPaint instances but received ${this::class}" } return internalSkiaPaint } @@ -168,5 +165,3 @@ internal class SkiaBackedPaint( else -> SkPaintStrokeJoin.MITER } } - -actual fun BlendMode.isSupported(): Boolean = true diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.nonAndroid.kt similarity index 98% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.nonAndroid.kt index 98afd5a595651..c99d18c48e6d2 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.nonAndroid.kt @@ -14,19 +14,23 @@ * limitations under the License. */ +@file:JvmName("SkiaBackedPath_skikoKt") +@file:JvmMultifileClass + package androidx.compose.ui.graphics import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.RoundRect +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName import org.jetbrains.skia.Path as SkPath import org.jetbrains.skia.PathDirection import org.jetbrains.skia.PathBuilder import org.jetbrains.skia.PathFillMode import org.jetbrains.skia.PathOp -actual fun Path(): Path = SkiaBackedPath() /** * Convert the [org.jetbrains.skia.Path] instance into a Compose-compatible Path @@ -43,7 +47,7 @@ fun SkPath.asComposePath(): Path = SkiaBackedPath(this).also { * It throws an exception if accessed on unsupported types. */ fun Path.asSkiaPath(): SkPath { - requirePrecondition(this is SkiaBackedPath) { + require(this is SkiaBackedPath) { "Extracting skia path reference is only supported from androidx.compose.ui.graphics.SkiaBackedPath instances but received ${this::class}" } isSkiaPathObserved = true @@ -62,7 +66,7 @@ fun Path.asSkiaPath(): SkPath { */ @InternalComposeUiApi fun Path.materializeSkiaPath(): SkPath { - requirePrecondition(this is SkiaBackedPath) { + require(this is SkiaBackedPath) { "Materializing skia path snapshot is only supported from androidx.compose.ui.graphics.SkiaBackedPath instances but received ${this::class}" } synchronizeSkiaPathIfNeeded() diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.nonAndroid.kt similarity index 64% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.nonAndroid.kt index b4e5ea4b7b294..ea728f0038dec 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.nonAndroid.kt @@ -14,9 +14,11 @@ * limitations under the License. */ +@file:JvmName("SkiaBackedPathEffect_skikoKt") + package androidx.compose.ui.graphics -import androidx.compose.ui.InternalComposeUiApi +import kotlin.jvm.JvmName import org.jetbrains.skia.PathEffect as SkPathEffect internal class SkiaBackedPathEffect( @@ -34,39 +36,12 @@ fun SkPathEffect.asComposePathEffect(): PathEffect = SkiaBackedPathEffect(this) * It throws an exception if accessed on unsupported types. */ fun PathEffect.asSkiaPathEffect(): SkPathEffect { - requirePrecondition(this is SkiaBackedPathEffect) { + require(this is SkiaBackedPathEffect) { "Extracting skia path effect reference is only supported from androidx.compose.ui.graphics.SkiaBackedPathEffect instances but received ${this::class}" } return internalSkiaPathEffect } -internal actual fun actualCornerPathEffect(radius: Float): PathEffect = - SkiaBackedPathEffect(SkPathEffect.makeCorner(radius)) - -internal actual fun actualDashPathEffect( - intervals: FloatArray, - phase: Float -): PathEffect = SkiaBackedPathEffect(SkPathEffect.makeDash(intervals, phase)) - -internal actual fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect = - SkiaBackedPathEffect(outer.asSkiaPathEffect().makeCompose(inner.asSkiaPathEffect())) - -@OptIn(InternalComposeUiApi::class) -internal actual fun actualStampedPathEffect( - shape: Path, - advance: Float, - phase: Float, - style: StampedPathEffectStyle -): PathEffect = - SkiaBackedPathEffect( - SkPathEffect.makePath1D( - shape.materializeSkiaPath(), - advance, - phase, - style.toSkiaStampedPathEffectStyle() - ) - ) - internal fun StampedPathEffectStyle.toSkiaStampedPathEffectStyle(): SkPathEffect.Style = when (this) { StampedPathEffectStyle.Morph -> SkPathEffect.Style.MORPH diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.nonAndroid.kt similarity index 92% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.nonAndroid.kt index ca3f5aa2e841a..47905591c65b9 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.nonAndroid.kt @@ -14,10 +14,15 @@ * limitations under the License. */ +@file:JvmName("SkiaBackedPathMeasure_skikoKt") +@file:JvmMultifileClass + package androidx.compose.ui.graphics import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Offset +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName import org.jetbrains.skia.PathMeasure as SkPathMeasure /** @@ -31,7 +36,7 @@ fun SkPathMeasure.asComposePathEffect(): PathMeasure = SkiaBackedPathMeasure(thi * It throws an exception if accessed on unsupported types. */ fun PathMeasure.asSkiaPathMeasure(): SkPathMeasure { - requirePrecondition(this is SkiaBackedPathMeasure) { + require(this is SkiaBackedPathMeasure) { "Extracting skia path measure reference is only supported from androidx.compose.ui.graphics.SkiaBackedPathMeasure instances but received ${this::class}" } return internalSkiaPathMeasure @@ -52,7 +57,7 @@ internal class SkiaBackedPathMeasure( destination: Path, startWithMoveTo: Boolean ): Boolean { - requirePrecondition(destination is SkiaBackedPath) { + require(destination is SkiaBackedPath) { "Getting a segment into a path is only supported for androidx.compose.ui.graphics.SkiaBackedPath instances but received ${destination::class}" } return destination.appendToPathBuilder { @@ -90,6 +95,3 @@ internal class SkiaBackedPathMeasure( } } } - -actual fun PathMeasure(): PathMeasure = - SkiaBackedPathMeasure() diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.nonAndroid.kt new file mode 100644 index 0000000000000..56775ef13e137 --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.nonAndroid.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(InternalComposeUiApi::class) +@file:JvmName("SkiaBackedRenderEffect_skikoKt") + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformRenderEffect +import kotlin.jvm.JvmName +import org.jetbrains.skia.ImageFilter + +internal class PlatformRenderEffectImpl( + val imageFilter: ImageFilter, +) : PlatformRenderEffect + +/** Convert the [org.jetbrains.skia.ImageFilter] instance into a Compose-compatible [RenderEffect] */ +fun ImageFilter.asComposeRenderEffect(): RenderEffect = + PlatformRenderEffectImpl(this).asComposeRenderEffect() + +/** + * Provides access to the underlying [org.jetbrains.skia.ImageFilter] instance. + * + * It throws an exception if accessed on unsupported types. + */ +val RenderEffect.skiaImageFilter: ImageFilter + get() = platformRenderEffect.skiaImageFilter + +/** Provides access to the underlying [org.jetbrains.skia.ImageFilter] of a platform binding. */ +internal val PlatformRenderEffect.skiaImageFilter: ImageFilter + get() { + require(this is PlatformRenderEffectImpl) { + "Extracting the Skia image filter reference is only supported from RenderEffects " + + "created by the registered Skiko implementation " + + "(registerSkikoComposeImplementation()), but the binding was " + + "${this::class}" + } + return imageFilter + } diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.nonAndroid.kt new file mode 100644 index 0000000000000..2a0c5789c04ed --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.nonAndroid.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(InternalComposeUiApi::class) +@file:JvmName("SkiaColorFilter_skikoKt") + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformColorFilter +import androidx.compose.ui.graphics.platform.asComposeColorFilter +import androidx.compose.ui.graphics.platform.platformColorFilter +import kotlin.jvm.JvmName +import org.jetbrains.skia.ColorFilter as SkColorFilter + +internal class PlatformColorFilterImpl( + val skiaColorFilter: SkColorFilter, +) : PlatformColorFilter + +/** Obtain a [org.jetbrains.skia.ColorFilter] instance from this [ColorFilter] */ +@OptIn(InternalComposeUiApi::class) +fun ColorFilter.asSkiaColorFilter(): SkColorFilter { + val platform = platformColorFilter + require(platform is PlatformColorFilterImpl) { + "Extracting the Skia color filter reference is only supported from ColorFilters created " + + "by the registered Skiko implementation (registerSkikoComposeImplementation()), but " + + "the binding was ${platform::class}" + } + return platform.skiaColorFilter +} + +/** Create a [ColorFilter] from the given [org.jetbrains.skia.ColorFilter] instance */ +@OptIn(InternalComposeUiApi::class) +fun SkColorFilter.asComposeColorFilter(): ColorFilter = + PlatformColorFilterImpl(this).asComposeColorFilter() diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaConversions.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaConversions.nonAndroid.kt new file mode 100644 index 0000000000000..780127b807b18 --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaConversions.nonAndroid.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import kotlin.math.PI +import org.jetbrains.skia.BlendMode as SkBlendMode +import org.jetbrains.skia.FilterTileMode +import org.jetbrains.skia.Matrix33 +import org.jetbrains.skia.VertexMode as SkVertexMode + +internal fun degrees(radians: Float): Float = (180f / PI.toFloat()) * radians + +internal fun BlendMode.toSkia() = when (this) { + BlendMode.Clear -> SkBlendMode.CLEAR + BlendMode.Src -> SkBlendMode.SRC + BlendMode.Dst -> SkBlendMode.DST + BlendMode.SrcOver -> SkBlendMode.SRC_OVER + BlendMode.DstOver -> SkBlendMode.DST_OVER + BlendMode.SrcIn -> SkBlendMode.SRC_IN + BlendMode.DstIn -> SkBlendMode.DST_IN + BlendMode.SrcOut -> SkBlendMode.SRC_OUT + BlendMode.DstOut -> SkBlendMode.DST_OUT + BlendMode.SrcAtop -> SkBlendMode.SRC_ATOP + BlendMode.DstAtop -> SkBlendMode.DST_ATOP + BlendMode.Xor -> SkBlendMode.XOR + BlendMode.Plus -> SkBlendMode.PLUS + BlendMode.Modulate -> SkBlendMode.MODULATE + BlendMode.Screen -> SkBlendMode.SCREEN + BlendMode.Overlay -> SkBlendMode.OVERLAY + BlendMode.Darken -> SkBlendMode.DARKEN + BlendMode.Lighten -> SkBlendMode.LIGHTEN + BlendMode.ColorDodge -> SkBlendMode.COLOR_DODGE + BlendMode.ColorBurn -> SkBlendMode.COLOR_BURN + BlendMode.Hardlight -> SkBlendMode.HARD_LIGHT + BlendMode.Softlight -> SkBlendMode.SOFT_LIGHT + BlendMode.Difference -> SkBlendMode.DIFFERENCE + BlendMode.Exclusion -> SkBlendMode.EXCLUSION + BlendMode.Multiply -> SkBlendMode.MULTIPLY + BlendMode.Hue -> SkBlendMode.HUE + BlendMode.Saturation -> SkBlendMode.SATURATION + BlendMode.Color -> SkBlendMode.COLOR + BlendMode.Luminosity -> SkBlendMode.LUMINOSITY + else -> SkBlendMode.SRC_OVER +} + +internal fun TileMode.toSkiaTileMode(): FilterTileMode = when (this) { + TileMode.Clamp -> FilterTileMode.CLAMP + TileMode.Repeated -> FilterTileMode.REPEAT + TileMode.Mirror -> FilterTileMode.MIRROR + TileMode.Decal -> FilterTileMode.DECAL + else -> FilterTileMode.CLAMP +} + +internal fun VertexMode.toSkiaVertexMode(): SkVertexMode = when (this) { + VertexMode.Triangles -> SkVertexMode.TRIANGLES + VertexMode.TriangleStrip -> SkVertexMode.TRIANGLE_STRIP + VertexMode.TriangleFan -> SkVertexMode.TRIANGLE_FAN + else -> SkVertexMode.TRIANGLES +} + +internal fun identityMatrix33() = Matrix33( + 1f, 0f, 0f, + 0f, 1f, 0f, + 0f, 0f, 1f +) + +internal fun Matrix.setFrom(matrix: Matrix33) { + val v = values + val m = matrix.mat + val scaleX = m[0] // MSCALE_X + val skewX = m[1] // MSKEW_X + val translateX = m[2] // MTRANS_X + val skewY = m[3] // MSKEW_Y + val scaleY = m[4] // MSCALE_Y + val translateY = m[5] // MTRANS_Y + val persp0 = m[6] // MPERSP_0 + val persp1 = m[7] // MPERSP_1 + val persp2 = m[8] // MPERSP_2 + + v[Matrix.ScaleX] = scaleX // 0 + v[Matrix.SkewY] = skewY // 1 + v[2] = 0f // 2 + v[Matrix.Perspective0] = persp0 // 3 + v[Matrix.SkewX] = skewX // 4 + v[Matrix.ScaleY] = scaleY // 5 + v[6] = 0f // 6 + v[Matrix.Perspective1] = persp1 // 7 + v[8] = 0f // 8 + v[9] = 0f // 9 + v[Matrix.ScaleZ] = 1.0f // 10 + v[11] = 0f // 11 + v[Matrix.TranslateX] = translateX // 12 + v[Matrix.TranslateY] = translateY // 13 + v[14] = 0f // 14 + v[Matrix.Perspective2] = persp2 // 15 +} + +internal fun Matrix33.setFrom(matrix: Matrix) { + val scaleX = matrix.values[Matrix.ScaleX] + val skewY = matrix.values[Matrix.SkewY] + val value2 = matrix.values[2] + val persp0 = matrix.values[Matrix.Perspective0] + val skewX = matrix.values[Matrix.SkewX] + val scaleY = matrix.values[Matrix.ScaleY] + val value6 = matrix.values[6] + val persp1 = matrix.values[Matrix.Perspective1] + val value8 = matrix.values[8] + + val translateX = matrix.values[Matrix.TranslateX] + val translateY = matrix.values[Matrix.TranslateY] + val persp2 = matrix.values[Matrix.Perspective2] + + val values = matrix.values + values[0] = scaleX + values[1] = skewX + values[2] = translateX + values[3] = skewY + values[4] = scaleY + values[5] = translateY + values[6] = persp0 + values[7] = persp1 + values[8] = persp2 + + for (index in 0..8) { + mat[index] = values[index] + } + + values[Matrix.ScaleX] = scaleX + values[Matrix.SkewY] = skewY + values[2] = value2 + values[Matrix.Perspective0] = persp0 + values[Matrix.SkewX] = skewX + values[Matrix.ScaleY] = scaleY + values[6] = value6 + values[Matrix.Perspective1] = persp1 + values[8] = value8 +} + +// Constant used to convert blur radius into a corresponding sigma value +// for the gaussian blur algorithm used within SkImageFilter. +// This constant approximates the scaling done in the software path's +// "high quality" mode, in SkBlurMask::Blur() (1 / sqrt(3)). +private val BlurSigmaScale = 0.57735f + +internal fun BlurEffect.Companion.convertRadiusToSigma(radius: Float) = + if (radius > 0) { + BlurSigmaScale * radius + 0.5f + } else { + 0.0f + } diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaGraphicsContext.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaGraphicsContext.nonAndroid.kt new file mode 100644 index 0000000000000..a451e6ab2f637 --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaGraphicsContext.nonAndroid.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.layer.SkikoGraphicsLayer +import androidx.compose.ui.graphics.platform.PlatformGraphicsContext +import androidx.compose.ui.graphics.platform.PlatformGraphicsLayer +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry +import org.jetbrains.skiko.node.RenderNode +import org.jetbrains.skiko.node.RenderNodeContext + +@InternalComposeUiApi +class SkiaGraphicsContext( + measureDrawBounds: Boolean = false, +) : PlatformGraphicsContext() { + init { + PlatformGraphicsRegistry.checkIfRegistered(SkikoGraphicsImpl) + } + + private val renderNodeContext = RenderNodeContext( + measureDrawBounds = measureDrawBounds, + ) + + override fun close() { + super.close() + renderNodeContext.close() + } + + override fun setLightingInfo( + centerX: Float, + centerY: Float, + centerZ: Float, + radius: Float, + ambientShadowAlpha: Float, + spotShadowAlpha: Float, + ) { + super.setLightingInfo(centerX, centerY, centerZ, radius, ambientShadowAlpha, spotShadowAlpha) + renderNodeContext.setLightingInfo( + centerX, + centerY, + centerZ, + radius, + ambientShadowAlpha, + spotShadowAlpha, + ) + } + + override fun createPlatformGraphicsLayer(): PlatformGraphicsLayer { + val renderNode = RenderNode(renderNodeContext) + return SkikoGraphicsLayer(renderNode) + } +} diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.nonAndroid.kt similarity index 78% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.nonAndroid.kt index 5fa2faf2516cd..582181d51927b 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.nonAndroid.kt @@ -14,14 +14,18 @@ * limitations under the License. */ +@file:JvmName("SkiaImageAsset_skikoKt") + package androidx.compose.ui.graphics import androidx.compose.ui.graphics.colorspace.ColorSpace import androidx.compose.ui.graphics.colorspace.ColorSpaces +import kotlin.jvm.JvmName import kotlin.math.abs import org.jetbrains.skia.Bitmap import org.jetbrains.skia.ColorAlphaType import org.jetbrains.skia.ColorInfo +import org.jetbrains.skia.ColorSpace as SkColorSpace import org.jetbrains.skia.ColorType import org.jetbrains.skia.Image import org.jetbrains.skia.ImageInfo @@ -42,24 +46,6 @@ fun Image.toComposeImageBitmap(): ImageBitmap = SkiaBackedImageBitmap(toBitmap() // See web implementation for details and the reason. internal expect fun Image.toBitmap(): Bitmap -internal actual fun ActualImageBitmap( - width: Int, - height: Int, - config: ImageBitmapConfig, - hasAlpha: Boolean, - colorSpace: ColorSpace -): ImageBitmap { - require(width > 0 && height > 0) { "width and height must be > 0" } - val colorType = config.toSkiaColorType() - val alphaType = if (hasAlpha) ColorAlphaType.PREMUL else ColorAlphaType.OPAQUE - val skiaColorSpace = colorSpace.toSkiaColorSpace() - val colorInfo = ColorInfo(colorType, alphaType, skiaColorSpace) - val imageInfo = ImageInfo(colorInfo, width, height) - val bitmap = Bitmap() - bitmap.allocPixels(imageInfo) - return SkiaBackedImageBitmap(bitmap) -} - /** * Obtain a reference to the [org.jetbrains.skia.Bitmap] * @@ -102,7 +88,7 @@ private class SkiaBackedImageBitmap(val bitmap: Bitmap) : ImageBitmap { val colorInfo = ColorInfo( ColorType.BGRA_8888, ColorAlphaType.UNPREMUL, - org.jetbrains.skia.ColorSpace.sRGB + SkColorSpace.sRGB ) val imageInfo = ImageInfo(colorInfo, width, height) val bytesPerPixel = 4 @@ -134,22 +120,22 @@ private fun ColorType.toComposeConfig() = when (this) { else -> ImageBitmapConfig.Argb8888 } -private fun org.jetbrains.skia.ColorSpace?.toComposeColorSpace(): ColorSpace { +private fun SkColorSpace?.toComposeColorSpace(): ColorSpace { return when (this) { - org.jetbrains.skia.ColorSpace.sRGB -> ColorSpaces.Srgb - org.jetbrains.skia.ColorSpace.sRGBLinear -> ColorSpaces.LinearSrgb - org.jetbrains.skia.ColorSpace.displayP3 -> ColorSpaces.DisplayP3 + SkColorSpace.sRGB -> ColorSpaces.Srgb + SkColorSpace.sRGBLinear -> ColorSpaces.LinearSrgb + SkColorSpace.displayP3 -> ColorSpaces.DisplayP3 else -> ColorSpaces.Srgb } } // TODO(demin): support all color spaces. // to do this we need to implement SkColorSpace::MakeRGB in skia -private fun ColorSpace.toSkiaColorSpace(): org.jetbrains.skia.ColorSpace { +private fun ColorSpace.toSkiaColorSpace(): SkColorSpace { return when (this) { - ColorSpaces.Srgb -> org.jetbrains.skia.ColorSpace.sRGB - ColorSpaces.LinearSrgb -> org.jetbrains.skia.ColorSpace.sRGBLinear - ColorSpaces.DisplayP3 -> org.jetbrains.skia.ColorSpace.displayP3 - else -> org.jetbrains.skia.ColorSpace.sRGB + ColorSpaces.Srgb -> SkColorSpace.sRGB + ColorSpaces.LinearSrgb -> SkColorSpace.sRGBLinear + ColorSpaces.DisplayP3 -> SkColorSpace.displayP3 + else -> SkColorSpace.sRGB } -} \ No newline at end of file +} diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaShader.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaShader.nonAndroid.kt new file mode 100644 index 0000000000000..4adf94f3a87f0 --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaShader.nonAndroid.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(InternalComposeUiApi::class) +@file:JvmName("SkiaShader_skikoKt") + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformShader +import kotlin.jvm.JvmName +import org.jetbrains.skia.Shader as SkShader + +internal class SkikoShader( + val skiaShader: SkShader, +) : PlatformShader + +/** Convert the [org.jetbrains.skia.Shader] instance into a Compose-compatible Shader */ +fun SkShader.asComposeShader(): Shader = Shader(SkikoShader(this)) + +/** Provides access to the underlying [org.jetbrains.skia.Shader] instance. */ +val Shader.skiaShader: SkShader + get() { + val platform = platformShader + require(platform is SkikoShader) { + "Extracting the Skia shader reference is only supported from Shaders created by the " + + "registered Skiko implementation (registerSkikoComposeImplementation()), but the " + + "binding was ${platform::class}" + } + return platform.skiaShader + } + +internal fun transformSkikoShader(shader: Shader, matrix: Matrix): Shader = + shader.skiaShader + .makeWithLocalMatrix(identityMatrix33().apply { setFrom(matrix) }) + .asComposeShader() diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoCanvasImpl.nonAndroid.kt similarity index 95% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoCanvasImpl.nonAndroid.kt index a592f8d38d231..86f6259d00979 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoCanvasImpl.nonAndroid.kt @@ -14,10 +14,14 @@ * limitations under the License. */ +@file:OptIn(InternalComposeUiApi::class) +@file:JvmName("SkiaBackedCanvas_skikoKt") + package androidx.compose.ui.graphics import androidx.compose.runtime.InternalComposeApi import androidx.compose.ui.InternalComposeUiApi +import kotlin.jvm.JvmName import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.unit.IntOffset @@ -35,21 +39,6 @@ import org.jetbrains.skia.Paint as SkPaint import org.jetbrains.skia.SamplingMode import org.jetbrains.skia.impl.use -@Deprecated( - message = "Use direct reference to org.jetbrains.skia.Canvas instead of typealias", - replaceWith = ReplaceWith("Canvas", "org.jetbrains.skia.Canvas"), - level = DeprecationLevel.ERROR, -) -actual typealias NativeCanvas = SkCanvas - -internal actual fun ActualCanvas(image: ImageBitmap): Canvas { - val skiaBitmap = image.asSkiaBitmap() - require(!skiaBitmap.isImmutable) { - "Cannot draw on immutable ImageBitmap" - } - return SkiaBackedCanvas(SkCanvas(skiaBitmap)) -} - /** * Convert the [org.jetbrains.skia.Canvas] instance into a Compose-compatible Canvas */ @@ -62,7 +51,7 @@ fun SkCanvas.asComposeCanvas(): Canvas = SkiaBackedCanvas(this) */ val Canvas.skiaCanvas: SkCanvas get() { - requirePrecondition(this is SkiaBackedCanvas) { + require(this is SkiaBackedCanvas) { "Extracting skia canvas reference is only supported from androidx.compose.ui.graphics.SkiaBackedCanvas instances but received ${this::class}" } return internalSkiaCanvas @@ -149,7 +138,6 @@ internal class SkiaBackedCanvas( ) } - @OptIn(InternalComposeUiApi::class) override fun clipPath(path: Path, clipOp: ClipOp) { val antiAlias = true internalSkiaCanvas.clipPath(path.materializeSkiaPath(), clipOp.toSkia(), antiAlias) @@ -235,7 +223,6 @@ internal class SkiaBackedCanvas( ) } - @OptIn(InternalComposeUiApi::class) override fun drawPath(path: Path, paint: Paint) { internalSkiaCanvas.drawPath( path = path.materializeSkiaPath(), diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoGraphicsImplementation.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoGraphicsImplementation.nonAndroid.kt new file mode 100644 index 0000000000000..f6715aa0efc48 --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoGraphicsImplementation.nonAndroid.kt @@ -0,0 +1,309 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.colorspace.ColorSpace +import androidx.compose.ui.graphics.colorspace.ColorSpaces +import androidx.compose.ui.graphics.platform.PlatformBlurFilter +import androidx.compose.ui.graphics.platform.PlatformColorFilter +import androidx.compose.ui.graphics.platform.PlatformGraphics +import androidx.compose.ui.graphics.platform.PlatformRenderEffect +import androidx.compose.ui.graphics.shadow.SkikoBlurFilterImplementation +import org.jetbrains.skia.Bitmap +import org.jetbrains.skia.Canvas as SkCanvas +import org.jetbrains.skia.Color4f +import org.jetbrains.skia.ColorAlphaType +import org.jetbrains.skia.ColorFilter as SkColorFilter +import org.jetbrains.skia.ColorInfo +import org.jetbrains.skia.ColorMatrix as SkColorMatrix +import org.jetbrains.skia.ColorSpace as SkColorSpace +import org.jetbrains.skia.ColorType +import org.jetbrains.skia.FilterBlurMode +import org.jetbrains.skia.Gradient +import org.jetbrains.skia.Image +import org.jetbrains.skia.ImageFilter +import org.jetbrains.skia.ImageInfo +import org.jetbrains.skia.MaskFilter +import org.jetbrains.skia.Paint as SkPaint +import org.jetbrains.skia.PathEffect as SkPathEffect +import org.jetbrains.skia.Shader as SkShader + +/** + * Skiko-side implementation entry point. Registration is intentionally kept separate. + */ +@OptIn(InternalComposeUiApi::class) +internal object SkikoGraphicsImpl : PlatformGraphics { + override fun createCanvas(image: ImageBitmap): Canvas { + val skiaBitmap = image.asSkiaBitmap() + require(!skiaBitmap.isImmutable) { + "Cannot draw on immutable ImageBitmap" + } + return SkCanvas(skiaBitmap).asComposeCanvas() + } + + override fun createPaint(): Paint = SkPaint().asComposePaint() + + // Skia supports all Compose blend and tile modes. + override fun isBlendModeSupported(blendMode: BlendMode): Boolean = true + + override fun isTileModeSupported(tileMode: TileMode): Boolean = true + + override fun createPath(): Path = SkiaBackedPath() + + override fun createPathIterator( + path: Path, + conicEvaluation: PathIterator.ConicEvaluation, + tolerance: Float, + ): PathIterator = SkiaPathIterator(path, conicEvaluation, tolerance) + + override fun createPathMeasure(): PathMeasure = + SkiaBackedPathMeasure() + + override fun createImageBitmap( + width: Int, + height: Int, + config: ImageBitmapConfig, + hasAlpha: Boolean, + colorSpace: ColorSpace, + ): ImageBitmap { + require(width > 0 && height > 0) { "width and height must be > 0" } + val colorInfo = ColorInfo( + config.toSkiaColorType(), + if (hasAlpha) ColorAlphaType.PREMUL else ColorAlphaType.OPAQUE, + colorSpace.toSkiaColorSpace(), + ) + val bitmap = Bitmap() + bitmap.allocPixels(ImageInfo(colorInfo, width, height)) + return bitmap.asComposeImageBitmap() + } + + override fun decodeImageBitmap(bytes: ByteArray): ImageBitmap = + Image.makeFromEncoded(bytes).toComposeImageBitmap() + + override fun createLinearGradientShader( + from: Offset, + to: Offset, + colors: List, + colorStops: List?, + tileMode: TileMode, + ): androidx.compose.ui.graphics.Shader { + validateColorStops(colors, colorStops) + return SkShader.makeLinearGradient( + from.x, + from.y, + to.x, + to.y, + colors.toSkiaGradient( + colorStops = colorStops, + tileMode = tileMode, + ), + ).asComposeShader() + } + + override fun createRadialGradientShader( + center: Offset, + radius: Float, + colors: List, + colorStops: List?, + tileMode: TileMode, + ): androidx.compose.ui.graphics.Shader { + validateColorStops(colors, colorStops) + return SkShader.makeRadialGradient( + center.x, + center.y, + radius, + colors.toSkiaGradient( + colorStops = colorStops, + tileMode = tileMode, + ), + ).asComposeShader() + } + + override fun createSweepGradientShader( + center: Offset, + colors: List, + colorStops: List?, + ): androidx.compose.ui.graphics.Shader { + validateColorStops(colors, colorStops) + return SkShader.makeSweepGradient( + center.x, + center.y, + colors.toSkiaGradient(colorStops = colorStops), + ).asComposeShader() + } + + override fun createImageShader( + image: ImageBitmap, + tileModeX: TileMode, + tileModeY: TileMode, + ): androidx.compose.ui.graphics.Shader = + image.asSkiaBitmap() + .makeShader(tileModeX.toSkiaTileMode(), tileModeY.toSkiaTileMode()) + .asComposeShader() + + override fun createCompositeShader( + destination: androidx.compose.ui.graphics.Shader, + source: androidx.compose.ui.graphics.Shader, + blendMode: BlendMode, + ): androidx.compose.ui.graphics.Shader = + SkShader.makeBlend(blendMode.toSkia(), destination.skiaShader, source.skiaShader) + .asComposeShader() + + override fun transformShader(shader: Shader, matrix: Matrix): Shader = + transformSkikoShader(shader, matrix) + + override fun createBlurRenderEffect( + renderEffect: RenderEffect?, + radiusX: Float, + radiusY: Float, + edgeTreatment: TileMode, + ): PlatformRenderEffect = + PlatformRenderEffectImpl( + if (renderEffect == null) { + ImageFilter.makeBlur( + BlurEffect.convertRadiusToSigma(radiusX), + BlurEffect.convertRadiusToSigma(radiusY), + edgeTreatment.toSkiaTileMode(), + ) + } else { + ImageFilter.makeBlur( + BlurEffect.convertRadiusToSigma(radiusX), + BlurEffect.convertRadiusToSigma(radiusY), + edgeTreatment.toSkiaTileMode(), + renderEffect.skiaImageFilter, + null, + ) + } + ) + + override fun createOffsetRenderEffect( + renderEffect: RenderEffect?, + offset: Offset, + ): PlatformRenderEffect = + PlatformRenderEffectImpl( + ImageFilter.makeOffset(offset.x, offset.y, renderEffect?.skiaImageFilter, null) + ) + + override fun createBlurFilter(radius: Float): PlatformBlurFilter = + SkikoBlurFilterImplementation( + MaskFilter.makeBlur(FilterBlurMode.NORMAL, BlurEffect.convertRadiusToSigma(radius)) + ) + + override fun setBlurFilter(paint: Paint, blur: PlatformBlurFilter?) { + require(blur == null || blur is SkikoBlurFilterImplementation) { + "Expected SkikoBlurFilterImplementation, got ${blur?.let { it::class }}" + } + paint.skiaPaint.maskFilter = blur?.maskFilter + } + + override fun createTintColorFilter(color: Color, blendMode: BlendMode): PlatformColorFilter = + PlatformColorFilterImpl(SkColorFilter.makeBlend(color.toArgb(), blendMode.toSkia())) + + override fun createColorMatrixColorFilter(colorMatrix: ColorMatrix): PlatformColorFilter { + // Skia applies the color-matrix translation column unscaled (0..1) while Compose uses + // 0..255, so rescale those entries before handing the matrix to skia. + val remappedValues = colorMatrix.values.copyOf() + remappedValues[4] *= (1f / 255f) + remappedValues[9] *= (1f / 255f) + remappedValues[14] *= (1f / 255f) + remappedValues[19] *= (1f / 255f) + return PlatformColorFilterImpl(SkColorFilter.makeMatrix(SkColorMatrix(remappedValues))) + } + + override fun createLightingColorFilter(multiply: Color, add: Color): PlatformColorFilter = + PlatformColorFilterImpl(SkColorFilter.makeLighting(multiply.toArgb(), add.toArgb())) + + // TODO: https://youtrack.jetbrains.com/issue/CMP-739 + override fun colorMatrixFromFilter(filter: PlatformColorFilter): ColorMatrix = ColorMatrix() + + override fun createCornerPathEffect(radius: Float): PathEffect = + SkPathEffect.makeCorner(radius).asComposePathEffect() + + override fun createDashPathEffect( + intervals: FloatArray, + phase: Float, + ): PathEffect = + SkPathEffect.makeDash(intervals, phase).asComposePathEffect() + + override fun createChainPathEffect( + outer: PathEffect, + inner: PathEffect, + ): PathEffect = + outer.asSkiaPathEffect().makeCompose(inner.asSkiaPathEffect()).asComposePathEffect() + + override fun createStampedPathEffect( + shape: Path, + advance: Float, + phase: Float, + style: StampedPathEffectStyle, + ): PathEffect = + SkPathEffect.makePath1D( + shape.materializeSkiaPath(), + advance, + phase, + style.toSkiaStampedPathEffectStyle(), + ).asComposePathEffect() +} + +private fun ImageBitmapConfig.toSkiaColorType() = when (this) { + ImageBitmapConfig.Argb8888 -> ColorType.N32 + ImageBitmapConfig.Alpha8 -> ColorType.ALPHA_8 + ImageBitmapConfig.Rgb565 -> ColorType.RGB_565 + ImageBitmapConfig.F16 -> ColorType.RGBA_F16 + else -> throw IllegalArgumentException("Unsupported ImageBitmapConfig: $this") +} + +private fun ColorSpace.toSkiaColorSpace(): SkColorSpace = when (this) { + ColorSpaces.Srgb -> SkColorSpace.sRGB + ColorSpaces.LinearSrgb -> SkColorSpace.sRGBLinear + ColorSpaces.DisplayP3 -> SkColorSpace.displayP3 + else -> SkColorSpace.sRGB +} + +private fun List.toSkiaGradient( + colorStops: List?, + tileMode: TileMode = TileMode.Clamp, +): Gradient = Gradient( + colors = Gradient.Colors( + colors = toColor4fArray(), + positions = colorStops?.toFloatArray(), + tileMode = tileMode.toSkiaTileMode(), + ), + interpolation = Gradient.Interpolation( + inPremul = Gradient.Interpolation.InPremul.YES, + ), +) + +private fun List.toColor4fArray(): Array = + Array(size) { i -> + val color = this[i] + Color4f(color.red, color.green, color.blue, color.alpha) + } + +private fun validateColorStops(colors: List, colorStops: List?) { + if (colorStops == null) { + require(colors.size >= 2) { + "colors must have length of at least 2 if colorStops is omitted." + } + } else { + require(colors.size == colorStops.size) { + "colors and colorStops arguments must have equal length." + } + } +} diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaPathIterator.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoPathIteratorImpl.nonAndroid.kt similarity index 96% rename from compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaPathIterator.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoPathIteratorImpl.nonAndroid.kt index 60e7131a66c12..aee8216afed8d 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaPathIterator.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoPathIteratorImpl.nonAndroid.kt @@ -1,3 +1,5 @@ +@file:OptIn(InternalComposeUiApi::class) + /* * Copyright 2024 The Android Open Source Project * @@ -19,12 +21,6 @@ package androidx.compose.ui.graphics import androidx.compose.ui.InternalComposeUiApi import org.jetbrains.skia.PathVerb -actual fun PathIterator( - path: Path, - conicEvaluation: PathIterator.ConicEvaluation, - tolerance: Float -): PathIterator = SkiaPathIterator(path, conicEvaluation, tolerance) - // The code below would be used to handle conic to quadratic conversions. // The core Skia API exposed by Skiko accepts a "power of 2" number of // quadratics when calling Path.convertConicToQuads() but our APIs expose @@ -67,8 +63,7 @@ actual fun PathIterator( // return subdivisions // } -@OptIn(InternalComposeUiApi::class) -private class SkiaPathIterator( +internal class SkiaPathIterator( override val path: Path, override val conicEvaluation: PathIterator.ConicEvaluation, override val tolerance: Float @@ -139,6 +134,7 @@ private class SkiaPathIterator( } } + @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") // FIXME: Make [PathSegment] constructor public override fun next(): PathSegment { if (!hasNext()) return DoneSegment diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/layer/SkikoGraphicsLayer.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/layer/SkikoGraphicsLayer.nonAndroid.kt new file mode 100644 index 0000000000000..664b1f3f130ae --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/layer/SkikoGraphicsLayer.nonAndroid.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.layer + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.RenderEffect +import androidx.compose.ui.graphics.SkiaBackedCanvas +import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.asSkiaColorFilter +import androidx.compose.ui.graphics.drawscope.CanvasDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.draw +import androidx.compose.ui.graphics.materializeSkiaPath +import androidx.compose.ui.graphics.platform.PlatformGraphicsLayer +import androidx.compose.ui.graphics.skiaCanvas +import androidx.compose.ui.graphics.skiaImageFilter +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.toSkia +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.toSize +import org.jetbrains.skia.Paint as SkPaint +import org.jetbrains.skia.Point +import org.jetbrains.skia.Rect as SkRect +import org.jetbrains.skiko.node.RenderNode + +@OptIn(InternalComposeUiApi::class) +internal class SkikoGraphicsLayer( + renderNode: RenderNode, +) : PlatformGraphicsLayer { + private var renderNode: RenderNode? = renderNode + private val pictureDrawScope = CanvasDrawScope() + + override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto + set(value) { + field = value + updateLayerProperties() + } + + override var pivotOffset: Offset = Offset.Unspecified + set(value) { + field = value + renderNode?.pivot = Point(value.x, value.y) + } + + override var alpha: Float = 1f + set(value) { + field = value + renderNode?.alpha = value + updateLayerProperties() + } + + override var scaleX: Float = 1f + set(value) { + field = value + renderNode?.scaleX = value + } + + override var scaleY: Float = 1f + set(value) { + field = value + renderNode?.scaleY = value + } + + override var translationX: Float = 0f + set(value) { + field = value + renderNode?.translationX = value + } + + override var translationY: Float = 0f + set(value) { + field = value + renderNode?.translationY = value + } + + override var shadowElevation: Float = 0f + set(value) { + field = value + renderNode?.shadowElevation = value + } + + override var ambientShadowColor: Color = Color.Black + set(value) { + field = value + renderNode?.ambientShadowColor = value.toArgb() + } + + override var spotShadowColor: Color = Color.Black + set(value) { + field = value + renderNode?.spotShadowColor = value.toArgb() + } + + override var blendMode: BlendMode = BlendMode.SrcOver + set(value) { + field = value + updateLayerProperties() + } + + override var colorFilter: ColorFilter? = null + set(value) { + field = value + updateLayerProperties() + } + + override var rotationX: Float = 0f + set(value) { + field = value + renderNode?.rotationX = value + } + + override var rotationY: Float = 0f + set(value) { + field = value + renderNode?.rotationY = value + } + + override var rotationZ: Float = 0f + set(value) { + field = value + renderNode?.rotationZ = value + } + + override var cameraDistance: Float = DefaultCameraDistance + set(value) { + field = value + renderNode?.cameraDistance = value + } + + override var renderEffect: RenderEffect? = null + set(value) { + field = value + updateLayerProperties() + } + + override fun setBounds(topLeft: IntOffset, size: IntSize) { + renderNode?.bounds = SkRect.makeXYWH( + topLeft.x.toFloat(), + topLeft.y.toFloat(), + size.width.toFloat(), + size.height.toFloat() + ) + } + + override fun setOutline(outline: Outline?, clip: Boolean) { + val renderNode = renderNode ?: return + if (outline == null) { + renderNode.clip = false + renderNode.setClipPath(null) + } else { + renderNode.clip = clip + when (outline) { + is Outline.Rectangle -> renderNode.setClipRect( + outline.rect.left, + outline.rect.top, + outline.rect.right, + outline.rect.bottom, + antiAlias = true + ) + is Outline.Rounded -> renderNode.setClipRRect( + outline.roundRect.left, + outline.roundRect.top, + outline.roundRect.right, + outline.roundRect.bottom, + floatArrayOf( + outline.roundRect.topLeftCornerRadius.x, + outline.roundRect.topLeftCornerRadius.y, + outline.roundRect.topRightCornerRadius.x, + outline.roundRect.topRightCornerRadius.y, + outline.roundRect.bottomRightCornerRadius.x, + outline.roundRect.bottomRightCornerRadius.y, + outline.roundRect.bottomLeftCornerRadius.x, + outline.roundRect.bottomLeftCornerRadius.y + ), + antiAlias = true + ) + is Outline.Generic -> renderNode.setClipPath( + outline.path.materializeSkiaPath(), + antiAlias = true + ) + } + } + } + + override fun record( + density: Density, + layoutDirection: LayoutDirection, + layer: GraphicsLayer, + block: DrawScope.() -> Unit, + ) { + val renderNode = renderNode ?: return + val recordingCanvas = renderNode.beginRecording() + try { + val composeCanvas = recordingCanvas.asComposeCanvas() as SkiaBackedCanvas + composeCanvas.alphaMultiplier = + if (compositingStrategy == CompositingStrategy.ModulateAlpha) { + alpha + } else { + 1.0f + } + pictureDrawScope.draw( + density = density, + layoutDirection = layoutDirection, + canvas = composeCanvas, + size = layer.size.toSize(), + graphicsLayer = layer, + block = block, + ) + } finally { + renderNode.endRecording() + } + } + + override fun draw(canvas: Canvas) { + renderNode?.drawInto(canvas.skiaCanvas) + } + + override fun discardDisplayList() { + renderNode?.close() + renderNode = null + } + + override fun setOutsets(left: Int, top: Int, right: Int, bottom: Int) { + // TODO: https://youtrack.jetbrains.com/issue/CMP-10054/Implement-GraphicsLayer.setOutsets-method + } + + private fun updateLayerProperties() { + renderNode?.layerPaint = if (requiresLayer()) { + SkPaint().also { + it.setAlphaf(alpha) + it.imageFilter = renderEffect?.skiaImageFilter + it.colorFilter = colorFilter?.asSkiaColorFilter() + it.blendMode = blendMode.toSkia() + } + } else { + null + } + } + + private fun requiresLayer(): Boolean { + val alphaNeedsLayer = alpha < 1f && compositingStrategy != CompositingStrategy.ModulateAlpha + val hasColorFilter = colorFilter != null + val hasBlendMode = blendMode != BlendMode.SrcOver + val hasRenderEffect = renderEffect != null + val offscreenBufferRequested = compositingStrategy == CompositingStrategy.Offscreen + return alphaNeedsLayer || hasColorFilter || hasBlendMode || hasRenderEffect || + offscreenBufferRequested + } +} diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/shadow/SkikoBlurImpl.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/shadow/SkikoBlurImpl.nonAndroid.kt new file mode 100644 index 0000000000000..d05bbbbf33e93 --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/shadow/SkikoBlurImpl.nonAndroid.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.graphics.shadow + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.platform.PlatformBlurFilter +import org.jetbrains.skia.MaskFilter + +@OptIn(InternalComposeUiApi::class) +internal class SkikoBlurFilterImplementation( + val maskFilter: MaskFilter, +) : PlatformBlurFilter diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/platform/ComposeUiSkikoRuntime.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/platform/ComposeUiSkikoRuntime.nonAndroid.kt new file mode 100644 index 0000000000000..b4180d946f336 --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/platform/ComposeUiSkikoRuntime.nonAndroid.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.platform + +import androidx.annotation.VisibleForTesting +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.SkikoGraphicsCompat +import androidx.compose.ui.graphics.SkikoGraphicsCompatRegistry +import androidx.compose.ui.graphics.SkikoGraphicsImpl +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry +import androidx.compose.ui.graphics.platform.PlatformRenderEffect +import androidx.compose.ui.graphics.skiaImageFilter +import androidx.compose.ui.text.platform.PlatformTextRegistry +import androidx.compose.ui.text.SkikoTextImpl +import org.jetbrains.skia.ImageFilter + +@InternalComposeUiApi +fun registerSkikoComposeImplementation() { + PlatformGraphicsRegistry.register(SkikoGraphicsImpl) + PlatformTextRegistry.register(SkikoTextImpl) + @Suppress("DEPRECATION") + SkikoGraphicsCompatRegistry.register(SkikoGraphicsCompatImpl) +} + +/** + * Unregisters the Skiko implementation. Intended for tests to avoid leaking the registered + * implementation as global state across test cases. + */ +@VisibleForTesting +@InternalComposeUiApi +fun clearSkikoComposeImplementation() { + PlatformGraphicsRegistry.clear() + PlatformTextRegistry.clear() + @Suppress("DEPRECATION") + SkikoGraphicsCompatRegistry.clear() +} + +/** + * Temporary bridge backing the deprecated Skia-returning members that cannot be relocated to this + * module (see [SkikoGraphicsCompat]). Delegates to the proper `skiaImageFilter` extension. + */ +@OptIn(InternalComposeUiApi::class) +@Suppress("DEPRECATION") +private object SkikoGraphicsCompatImpl : SkikoGraphicsCompat { + override fun imageFilter(renderEffect: PlatformRenderEffect): ImageFilter = + renderEffect.skiaImageFilter +} diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Cache.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/Cache.nonAndroid.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Cache.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/Cache.nonAndroid.kt diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/ParagraphDefaultFontAccess.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/ParagraphDefaultFontAccess.nonAndroid.kt new file mode 100644 index 0000000000000..93e9a5a3527ee --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/ParagraphDefaultFontAccess.nonAndroid.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(InternalComposeUiApi::class) + +package androidx.compose.ui.text + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.text.PlatformParagraph +import org.jetbrains.skia.Font as SkFont + +/** + * Access to the default Skia font behind a paragraph implementation. Kept here (in :ui-skiko) so the + * skia type does not leak back into the skia-free ui-text `nonAndroidMain`. Used by tests. + */ +internal val PlatformParagraph.skiaDefaultFont: SkFont + get() = (this as PlatformParagraphImpl).defaultFont diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/SkiaParagraph.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/PlatformParagraphImpl.nonAndroid.kt similarity index 96% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/SkiaParagraph.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/PlatformParagraphImpl.nonAndroid.kt index 2749efa6cd7fd..f0cfe481dcfb5 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/SkiaParagraph.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/PlatformParagraphImpl.nonAndroid.kt @@ -14,9 +14,12 @@ * limitations under the License. */ +@file:OptIn(InternalComposeUiApi::class) + package androidx.compose.ui.text import org.jetbrains.skia.Rect as SkRect +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size @@ -30,8 +33,8 @@ import androidx.compose.ui.graphics.asComposePath import androidx.compose.ui.graphics.drawscope.DrawStyle import androidx.compose.ui.graphics.skiaCanvas import androidx.compose.ui.graphics.toComposeRect -import androidx.compose.ui.text.internal.requirePrecondition -import androidx.compose.ui.text.platform.SkiaParagraphIntrinsics +import androidx.compose.ui.text.PlatformParagraph +import androidx.compose.ui.text.platform.SkikoParagraphIntrinsics import androidx.compose.ui.text.platform.cursorHorizontalPosition import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.ResolvedTextDirection @@ -51,12 +54,12 @@ import org.jetbrains.skia.paragraph.RectWidthMode import org.jetbrains.skia.paragraph.TextBox import org.jetbrains.skia.paragraph.Paragraph as SkParagraph -internal class SkiaParagraph( - private val paragraphIntrinsics: SkiaParagraphIntrinsics, +internal class PlatformParagraphImpl( + private val paragraphIntrinsics: SkikoParagraphIntrinsics, val maxLines: Int, private val overflow: TextOverflow, val constraints: Constraints -) : Paragraph { +) : PlatformParagraph { private val layouter = paragraphIntrinsics.layouter().apply { setParagraphStyle(maxLines, ellipsis) } @@ -77,11 +80,11 @@ internal class SkiaParagraph( } init { - requirePrecondition(constraints.minHeight == 0 && constraints.minWidth == 0) { + require(constraints.minHeight == 0 && constraints.minWidth == 0) { "Setting Constraints.minWidth and Constraints.minHeight is not supported, " + "these should be the default zero values instead." } - requirePrecondition(maxLines >= 1) { "maxLines should be greater than 0" } + require(maxLines >= 1) { "maxLines should be greater than 0" } // Size is not known until layout is complete but to apply it, we need to re-create // skia's paragraph :'( @@ -160,7 +163,7 @@ internal class SkiaParagraph( } override fun getPathForRange(start: Int, end: Int): Path { - requirePrecondition(start in 0..end && end <= text.length) { + require(start in 0..end && end <= text.length) { "start($start) or end($end) is out of range [0..${text.length}]," + " or start > end!" } @@ -229,13 +232,13 @@ internal class SkiaParagraph( floor((line.baseline + line.descent).toFloat()) } ?: 0f - internal fun getLineAscent(lineIndex: Int): Float = + override fun getLineAscent(lineIndex: Int): Float = -(lineMetrics.getOrNull(lineIndex)?.ascent?.toFloat() ?: 0f) override fun getLineBaseline(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.baseline?.toFloat() ?: 0f - internal fun getLineDescent(lineIndex: Int): Float = + override fun getLineDescent(lineIndex: Int): Float = lineMetrics.getOrNull(lineIndex)?.descent?.toFloat() ?: 0f private fun lineMetricsForOffset(offset: Int): LineMetrics? = @@ -497,7 +500,7 @@ internal class SkiaParagraph( } override fun getBoundingBox(offset: Int): Rect { - requirePrecondition(offset in text.indices) { + require(offset in text.indices) { "offset($offset) is out of bounds [0,${text.length})" } val box = getBoxForwardByOffset(offset) ?: getBoxBackwardByOffset(offset, text.length)!! @@ -617,7 +620,7 @@ internal class SkiaParagraph( */ @Suppress("NOTHING_TO_INLINE") private inline fun checkOffsetIsValid(offset: Int) { - requirePrecondition(offset in 0..text.length) { + require(offset in 0..text.length) { "offset($offset) is out of bounds [0,${text.length}]" } } @@ -629,6 +632,7 @@ private fun LineMetrics.trimFirstAscent( ): LineMetrics { if (textStyle.lineHeight.isUnspecified) return this val style = textStyle.lineHeightStyle ?: LineHeightStyle.Default + @Suppress("INVISIBLE_REFERENCE") // FIXME: Make [isTrimFirstLineTop] public val ascent = if (style.trim.isTrimFirstLineTop()) { -fontMetrics.ascent.toDouble() } else { @@ -643,6 +647,7 @@ private fun LineMetrics.trimLastDescent( ): LineMetrics { if (textStyle.lineHeight.isUnspecified) return this val style = textStyle.lineHeightStyle ?: LineHeightStyle.Default + @Suppress("INVISIBLE_REFERENCE") // FIXME: Make [isTrimLastLineBottom] public val descent = if (style.trim.isTrimLastLineBottom()) { fontMetrics.descent.toDouble() } else { @@ -681,7 +686,7 @@ private fun LineMetrics.copy( lineNumber = lineNumber ) -private fun Paragraph.numberOfLinesThatFitMaxHeight(maxHeight: Int): Int { +private fun PlatformParagraphImpl.numberOfLinesThatFitMaxHeight(maxHeight: Int): Int { for (lineIndex in 0 until lineCount) { if (getLineBottom(lineIndex) > maxHeight) return lineIndex } diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/CharHelpers.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/SkikoCharHelpers.nonAndroid.kt similarity index 92% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/CharHelpers.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/SkikoCharHelpers.nonAndroid.kt index f2fce3c76a3dd..abc550fd90e4f 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/CharHelpers.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/SkikoCharHelpers.nonAndroid.kt @@ -20,16 +20,16 @@ import kotlin.experimental.ExperimentalNativeApi import kotlin.jvm.JvmInline import org.jetbrains.skia.BreakIterator -internal actual fun String.findPrecedingBreak(index: Int): Int { - val it = BreakIterator.makeCharacterInstance() - it.setText(this) - return it.preceding(index) +internal fun findSkikoPrecedingBreak(text: String, index: Int): Int { + val iterator = BreakIterator.makeCharacterInstance() + iterator.setText(text) + return iterator.preceding(index) } -internal actual fun String.findFollowingBreak(index: Int): Int { - val it = BreakIterator.makeCharacterInstance() - it.setText(this) - return it.following(index) +internal fun findSkikoFollowingBreak(text: String, index: Int): Int { + val iterator = BreakIterator.makeCharacterInstance() + iterator.setText(text) + return iterator.following(index) } /** diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/SkikoTextImpl.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/SkikoTextImpl.nonAndroid.kt new file mode 100644 index 0000000000000..a173ae27b8d0c --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/SkikoTextImpl.nonAndroid.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.text + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.SkiaFontLoader +import androidx.compose.ui.text.font.createPlatformFontFamilyResolver +import androidx.compose.ui.text.platform.Platform +import androidx.compose.ui.text.PlatformParagraph +import androidx.compose.ui.text.platform.PlatformText +import androidx.compose.ui.text.platform.SkikoParagraphIntrinsics +import androidx.compose.ui.text.platform.currentPlatform +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import kotlin.coroutines.CoroutineContext + +/** + * Skiko-side text implementation entry point. Registration is intentionally left to a follow-up + * integration change. + */ +@OptIn(InternalComposeUiApi::class) +internal object SkikoTextImpl : PlatformText { + override fun createParagraph( + text: String, + style: TextStyle, + annotations: List>, + placeholders: List>, + maxLines: Int, + overflow: TextOverflow, + constraints: Constraints, + density: Density, + fontFamilyResolver: FontFamily.Resolver, + ): PlatformParagraph = PlatformParagraphImpl( + SkikoParagraphIntrinsics( + text = text, + style = style, + annotations = annotations, + placeholders = placeholders, + density = density, + fontFamilyResolver = fontFamilyResolver, + ), + maxLines, + overflow, + constraints, + ) + + override fun createParagraph( + paragraphIntrinsics: ParagraphIntrinsics, + maxLines: Int, + overflow: TextOverflow, + constraints: Constraints, + ): PlatformParagraph = + PlatformParagraphImpl( + paragraphIntrinsics as SkikoParagraphIntrinsics, + maxLines, + overflow, + constraints, + ) + + override fun createParagraphIntrinsics( + text: String, + style: TextStyle, + annotations: List>, + placeholders: List>, + density: Density, + fontFamilyResolver: FontFamily.Resolver, + ): ParagraphIntrinsics = SkikoParagraphIntrinsics( + text = text, + style = style, + annotations = annotations, + placeholders = placeholders, + density = density, + fontFamilyResolver = fontFamilyResolver, + ) + + override fun createFontFamilyResolver(): FontFamily.Resolver = + createPlatformFontFamilyResolver(SkiaFontLoader()) + + @OptIn(ExperimentalTextApi::class) + override fun createFontFamilyResolver(coroutineContext: CoroutineContext): FontFamily.Resolver = + createPlatformFontFamilyResolver(SkiaFontLoader(), coroutineContext) + + override fun findPrecedingBreak(text: String, index: Int): Int = + findSkikoPrecedingBreak(text, index) + + override fun findFollowingBreak(text: String, index: Int): Int = + findSkikoFollowingBreak(text, index) + + @OptIn(ExperimentalTextApi::class) + override val defaultFontRasterizationSettings: FontRasterizationSettings by lazy { + when (currentPlatform()) { + Platform.Windows -> FontRasterizationSettings( + subpixelPositioning = true, + // Most UIs still use ClearType on Windows, so we should match this + // We temporarily disabled `SubpixelAntiAlias` until we figure out + // how to properly retrieve default OS settings + smoothing = FontSmoothing.AntiAlias, + hinting = FontHinting.Normal, // None would trigger some potentially unwanted behavior, but everything else is forced into Normal on Windows + autoHintingForced = false, + ) + + Platform.Linux, Platform.Unknown -> FontRasterizationSettings( + subpixelPositioning = true, + smoothing = FontSmoothing.AntiAlias, + hinting = FontHinting.Slight, // Most distributions use Slight now by default + autoHintingForced = false, + ) + + Platform.Android -> FontRasterizationSettings( + subpixelPositioning = true, + smoothing = FontSmoothing.AntiAlias, + hinting = FontHinting.Slight, + autoHintingForced = false, + ) + + Platform.MacOS, Platform.IOS, Platform.TvOS, Platform.WatchOS -> FontRasterizationSettings( + subpixelPositioning = true, + smoothing = FontSmoothing.AntiAlias, // macOS doesn't support SubpixelAntiAlias anymore as of Catalina + hinting = FontHinting.Normal, // Completely ignored on macOS + autoHintingForced = false, // Completely ignored on macOS + ) + } + } +} diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/SkiaFontLoader.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/SkiaFontLoader.nonAndroid.kt similarity index 81% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/SkiaFontLoader.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/SkiaFontLoader.nonAndroid.kt index acf5bf1c3a0ca..84adea6fc1352 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/SkiaFontLoader.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/SkiaFontLoader.nonAndroid.kt @@ -14,25 +14,31 @@ * limitations under the License. */ +@file:OptIn(InternalComposeUiApi::class) + package androidx.compose.ui.text.font +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.text.font.FontLoadingStrategy.Companion.Async import androidx.compose.ui.text.font.FontLoadingStrategy.Companion.Blocking import androidx.compose.ui.text.font.FontLoadingStrategy.Companion.OptionalLocal import androidx.compose.ui.text.platform.FontCache import androidx.compose.ui.text.platform.FontLoadResult import androidx.compose.ui.text.platform.PlatformFont -import org.jetbrains.skia.paragraph.FontCollection +/** + * Skia-backed [PlatformTypefacesLoader]. ui-text adapts this into its internal `PlatformFontLoader` + * via `createPlatformFontFamilyResolver`, so no skia type or ui-text internal leaks across modules. + */ internal class SkiaFontLoader( fontCacheProvider: () -> FontCache -) : PlatformFontLoader { +) : PlatformTypefacesLoader { constructor(fontCache: FontCache = FontCache()) : this (fontCacheProvider = { fontCache }) private val fontCache: FontCache by lazy(fontCacheProvider) - val fontCollection: FontCollection + override val fontCollection: Any get() = fontCache.fonts override fun loadBlocking(font: Font): FontLoadResult? { @@ -53,11 +59,11 @@ internal class SkiaFontLoader( } } - internal fun loadPlatformTypes( + override fun loadPlatformTypes( fontFamily: FontFamily, - fontWeight: FontWeight = FontWeight.Normal, - fontStyle: FontStyle = FontStyle.Normal - ): FontLoadResult = fontCache.loadPlatformTypes(fontFamily, fontWeight, fontStyle) + fontWeight: FontWeight, + fontStyle: FontStyle + ): Any = fontCache.loadPlatformTypes(fontFamily, fontWeight, fontStyle) override suspend fun awaitLoad(font: Font): FontLoadResult? { // TODO: This should actually do async loading, but for now desktop only supports local @@ -67,6 +73,6 @@ internal class SkiaFontLoader( return loadBlocking(font) } - override val cacheKey: Any + override val cacheKey: Any? get() = fontCache // results are valid for all shared caches } diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.nonAndroid.kt similarity index 96% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.nonAndroid.kt index f38b158fe0a13..74db6f7e01f26 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.nonAndroid.kt @@ -45,7 +45,7 @@ internal expect fun getUnresolvedSymbolsRegistry(): UnresolvedSymbolsRegistry? /** * The purpose of this class is to store already built paragraph and pass it between - * different internal entities (from SkiaParagraphIntrinsics to SkiaParagraph). + * different internal entities (from PlatformParagraphIntrinsics to PlatformParagraph). * * An alternative to passing and reusing existed paragraph is to build it again, but it is 2.5x * slower. @@ -183,8 +183,10 @@ internal class ParagraphLayouter( // Since it affects only [ShaderBrush] we can keep the cache if it's not used. if (builder.textStyle.brush is ShaderBrush || builder.annotations.fastAny { - it.item is SpanStyle && // TODO(ivan): Verify that we need only [SpanStyle] here - it.item.brush is ShaderBrush }) { + // TODO(ivan): Verify that we need only [SpanStyle] here + val spanStyle = it.item as? SpanStyle + spanStyle?.brush is ShaderBrush + }) { invalidateParagraph(onlyForeground = true) } } diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaTextPaint.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/ParagraphTextPaint.nonAndroid.kt similarity index 67% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaTextPaint.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/ParagraphTextPaint.nonAndroid.kt index a1324df1d8fb1..438ab1b43ed1e 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaTextPaint.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/ParagraphTextPaint.nonAndroid.kt @@ -22,21 +22,83 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.Shader import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.skiaPaint import androidx.compose.ui.graphics.drawscope.DrawStyle import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.graphics.isSpecified -import androidx.compose.ui.text.style.modulate +import androidx.compose.ui.text.SpanStyle + +internal data class ParagraphTextForegroundStyle( + val color: Color, + val brush: Brush?, + val alpha: Float, +) { + companion object { + val Unspecified = ParagraphTextForegroundStyle( + color = Color.Unspecified, + brush = null, + alpha = Float.NaN, + ) + } + + fun merge(other: ParagraphTextForegroundStyle): ParagraphTextForegroundStyle = + when { + other.brush is ShaderBrush && brush is ShaderBrush -> + ParagraphTextForegroundStyle(other.color, other.brush, other.alpha.takeOrElse { alpha }) + other.brush is ShaderBrush -> other + brush is ShaderBrush -> this + else -> if (other != Unspecified) other else this + } +} + +internal fun SpanStyle.toParagraphTextForegroundStyle(): ParagraphTextForegroundStyle { + val currentBrush = brush + return when (currentBrush) { + null -> { + if (color.isSpecified) { + ParagraphTextForegroundStyle( + color = color, + brush = null, + alpha = color.alpha, + ) + } else { + ParagraphTextForegroundStyle.Unspecified + } + } + + is SolidColor -> ParagraphTextForegroundStyle( + color = currentBrush.value.modulate(alpha), + brush = null, + alpha = currentBrush.value.alpha, + ) + + else -> ParagraphTextForegroundStyle( + color = Color.Unspecified, + brush = currentBrush, + alpha = alpha, + ) + } +} + +private fun Color.modulate(alpha: Float): Color = + when { + alpha.isNaN() || alpha >= 1f -> this + else -> copy(alpha = this.alpha * alpha) + } + +private fun Float.takeOrElse(block: () -> Float): Float = + if (isNaN()) block() else this // Copied from AndroidTextPaint. -internal class SkiaTextPaint( +internal class ParagraphTextPaint( private val original: Paint = Paint(), ) : Paint by original { internal val skiaPaint @@ -107,9 +169,6 @@ internal class SkiaTextPaint( } } - /** - * Clears all shader related cache parameters and native shader property. - */ private fun clearShader() { this.shaderState = null this.brush = null diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/Platform.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/Platform.nonAndroid.kt new file mode 100644 index 0000000000000..c0070a64ba6db --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/Platform.nonAndroid.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.text.platform + +// Host platform detection used by the skia-backed font subsystem. The per-platform actuals live in +// the platform leaves (desktop/web/native). +internal enum class Platform { + Unknown, + Linux, + Windows, + MacOS, + IOS, + TvOS, + WatchOS, + Android, // use case: a web app running in Chrome Android +} + +internal expect fun currentPlatform(): Platform diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.nonAndroid.kt similarity index 76% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.nonAndroid.kt index 5f8bef742188e..0a7f08e69e05a 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.nonAndroid.kt @@ -13,119 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +@file:JvmName("PlatformFont_skikoKt") +@file:OptIn(InternalComposeUiApi::class) + package androidx.compose.ui.text.platform -import org.jetbrains.skia.Typeface as SkTypeface +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.ExpireAfterAccessCache import androidx.compose.ui.text.InternalTextApi -import androidx.compose.ui.text.font.DefaultFontFamily import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontListFontFamily -import androidx.compose.ui.text.font.FontLoadingStrategy +import androidx.compose.ui.text.font.FontResourceLoaderWithResolver import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontVariation import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.GenericFontFamily import androidx.compose.ui.text.font.LoadedFontFamily +import androidx.compose.ui.text.font.SkiaFontLoader import androidx.compose.ui.text.font.Typeface -import androidx.compose.ui.text.font.createFontFamilyResolver +import androidx.compose.ui.text.font.createPlatformFontFamilyResolver import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMapNotNull +import kotlin.jvm.JvmName import org.jetbrains.skia.FontMgrWithFallback +import org.jetbrains.skia.FontVariation as SkFontVariation +import org.jetbrains.skia.Typeface as SkTypeface import org.jetbrains.skia.paragraph.FontCollection import org.jetbrains.skia.paragraph.TypefaceFontProviderWithFallback - -expect sealed class PlatformFont() : Font { - abstract val identity: String - abstract val variationSettings: FontVariation.Settings - internal val cacheKey: String -} - -/** - * A Font that's already installed in the system. - * - * @param identity Unique identity for a font. Used internally to distinguish fonts. - * @param weight The weight of the font. The system uses this to match a font to a font request - * that is given in a [androidx.compose.ui.text.SpanStyle]. - * @param style The style of the font, normal or italic. The system uses this to match a font to a - * font request that is given in a [androidx.compose.ui.text.SpanStyle]. - * - * @see FontFamily - */ -@ExperimentalTextApi -class SystemFont( - override val identity: String, - override val weight: FontWeight = FontWeight.Normal, - override val style: FontStyle = FontStyle.Normal, - override val variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style), -) : PlatformFont() { - - constructor( - identity: String, - weight: FontWeight = FontWeight.Normal, - style: FontStyle = FontStyle.Normal - ) : this(identity, weight, style, variationSettings = FontVariation.Settings()) - - override fun toString(): String { - return "SystemFont(identity='$identity', weight=$weight, style=$style, variationSettings=${variationSettings.settings})" - } -} - -/** - * Defines a Font using a byte array with loaded font data. - * - * @param identity Unique identity for a font. Used internally to distinguish fonts. - * @param getData should return Byte array with loaded font data. - * @param weight The weight of the font. The system uses this to match a font to a font request - * that is given in a [androidx.compose.ui.text.SpanStyle]. - * @param style The style of the font, normal or italic. The system uses this to match a font to a - * font request that is given in a [androidx.compose.ui.text.SpanStyle]. - * - * @see FontFamily - */ -class LoadedFont internal constructor( - override val identity: String, - internal val getData: () -> ByteArray, - override val weight: FontWeight, - override val style: FontStyle, - override val variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style), -) : PlatformFont() { - - constructor( - identity: String, - getData: () -> ByteArray, - weight: FontWeight, - style: FontStyle - ) : this(identity, getData, weight, style, FontVariation.Settings()) - - @ExperimentalTextApi - override val loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Blocking - - val data: ByteArray get() = getData() - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is LoadedFont) return false - if (identity != other.identity) return false - if (weight != other.weight) return false - if (style != other.style) return false - return variationSettings.settings == other.variationSettings.settings - } - - override fun hashCode(): Int { - var result = identity.hashCode() - result = 31 * result + weight.hashCode() - result = 31 * result + style.hashCode() - result = 31 * result + variationSettings.settings.hashCode() - return result - } - - override fun toString(): String { - return "LoadedFont(identity='$identity', weight=$weight, style=$style, variationSettings=${variationSettings.settings})" - } -} +import org.jetbrains.skiko.currentNanoTime /** * Creates a Font using byte array with loaded font data. @@ -178,14 +96,6 @@ fun Font( variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style) ): Font = LoadedFont(identity, getData, weight, style, variationSettings) -private class SkiaBackedTypeface( - alias: String?, - val nativeTypeface: SkTypeface -) : Typeface { - val alias = alias ?: nativeTypeface.familyName - override val fontFamily: FontFamily? = null -} - /** * Creates a Font using byte array with loaded font data. * @@ -249,6 +159,14 @@ fun Font( variationSettings = variationSettings, ) +private class SkiaBackedTypeface( + alias: String?, + val nativeTypeface: SkTypeface +) : Typeface { + val alias = alias ?: nativeTypeface.familyName + override val fontFamily: FontFamily? = null +} + /** * Returns a Compose [Typeface] from Skia [SkTypeface]. * @@ -264,11 +182,11 @@ fun Typeface(typeface: SkTypeface, alias: String? = null): Typeface { " should be replaced", ReplaceWith("PlatformFontLoader"), ) -class FontLoader : Font.ResourceLoader { +class FontLoader : Font.ResourceLoader, FontResourceLoaderWithResolver { private val fontCache: FontCache by lazy { FontCache() } - internal val fontFamilyResolver: FontFamily.Resolver by lazy { - createFontFamilyResolver(fontCache) + override val fontFamilyResolver: FontFamily.Resolver by lazy { + createPlatformFontFamilyResolver(SkiaFontLoader(fontCache)) } // TODO: we need to support: @@ -296,7 +214,8 @@ internal class FontCache { private val fontProvider = TypefaceFontProviderWithFallback() private val registered: MutableSet = HashSet() private val typefacesCache = ExpireAfterAccessCache( - 60_000_000_000 // 1 minute + 60_000_000_000, // 1 minute + ::currentNanoTime, ) init { @@ -330,8 +249,11 @@ internal class FontCache { } } - private fun ensureRegistered(fontFamily: FontFamily): List = - when (fontFamily) { + private fun ensureRegistered(fontFamily: FontFamily): List { + // FontFamily.Default is the only DefaultFontFamily instance (its type is commonMain-internal, + // so it is matched by identity through the public FontFamily.Default). + if (fontFamily == FontFamily.Default) return FontFamily.SansSerif.aliases + return when (fontFamily) { is FontListFontFamily -> { val fonts = fontFamily.fonts.fastMapNotNull { if (it is SystemFont) it.identity @@ -352,22 +274,11 @@ internal class FontCache { listOf(typeface.alias) } is GenericFontFamily -> fontFamily.aliases - is DefaultFontFamily -> FontFamily.SansSerif.aliases + else -> error("Unsupported font family: $fontFamily") } + } } -internal enum class Platform { - Unknown, - Linux, - Windows, - MacOS, - IOS, - TvOS, - WatchOS, - Android, // use case: a web app running in Chrome Android -} - -internal expect fun currentPlatform(): Platform internal expect fun loadTypeface(font: Font): SkTypeface internal val GenericFontFamily.aliases @@ -419,9 +330,9 @@ private val GenericFontFamiliesMapping: Map> by lazy { } } -internal fun FontVariation.Settings.toSkiaFontVariationList(): List { +internal fun FontVariation.Settings.toSkiaFontVariationList(): List { return settings.fastMap { setting -> - org.jetbrains.skia.FontVariation(setting.axisName, setting.toVariationValue(null)) + SkFontVariation(setting.axisName, setting.toVariationValue(null)) } } @@ -435,4 +346,4 @@ internal fun SkTypeface.cloneWithVariationSettings(variationSettings: FontVariat if (variationSettings.settings.isEmpty()) return this val variations = variationSettings.toSkiaFontVariationList() return makeClone(variations.toTypedArray()) -} \ No newline at end of file +} diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.nonAndroid.kt new file mode 100644 index 0000000000000..16dcdfc289baa --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.nonAndroid.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("SkiaParagraph_skikoKt") +@file:JvmMultifileClass + +package androidx.compose.ui.text.platform + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.platformTypefacesLoader +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName +import org.jetbrains.skia.paragraph.FontCollection +import org.jetbrains.skia.FontStyle as SkFontStyle + +@OptIn(InternalComposeUiApi::class) +internal fun resolveParagraphFontCollection( + fontFamilyResolver: FontFamily.Resolver +): FontCollection { + val backend = fontFamilyResolver.platformTypefacesLoader() + ?: throw IllegalStateException("Unsupported font loader for $fontFamilyResolver") + return backend.fontCollection as FontCollection +} + +internal fun FontStyle.toSkFontStyle(): SkFontStyle { + return when (this) { + FontStyle.Italic -> SkFontStyle.ITALIC + else -> SkFontStyle.NORMAL + } +} diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/ParagraphBuilder.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkikoParagraphBuilder.nonAndroid.kt similarity index 93% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/ParagraphBuilder.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkikoParagraphBuilder.nonAndroid.kt index 085ed11300ddc..c0290c3c2905f 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/ParagraphBuilder.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkikoParagraphBuilder.nonAndroid.kt @@ -14,12 +14,11 @@ * limitations under the License. */ -@file:OptIn(ExperimentalTextApi::class, InternalTextApi::class) -@file:JvmName("SkiaParagraph_skikoKt") -@file:JvmMultifileClass +@file:OptIn(ExperimentalTextApi::class, InternalTextApi::class, InternalComposeUiApi::class) package androidx.compose.ui.text.platform +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color @@ -30,7 +29,9 @@ import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.FontHinting import androidx.compose.ui.text.FontRasterizationSettings +import androidx.compose.ui.text.FontSmoothing import androidx.compose.ui.text.InternalTextApi import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign @@ -40,21 +41,16 @@ import androidx.compose.ui.text.TextDecorationLineStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.WeakKeysCache import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontFamilyResolverImpl import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontSynthesis import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.SkiaFontLoader import androidx.compose.ui.text.intl.LocaleList import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.ResolvedTextDirection import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.style.TextForegroundStyle import androidx.compose.ui.text.style.TextGeometricTransform -import androidx.compose.ui.text.toSkFontEdging -import androidx.compose.ui.text.toSkFontHinting import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.isSpecified @@ -62,11 +58,11 @@ import androidx.compose.ui.unit.isUnspecified import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachReversed -import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName import org.jetbrains.skia.Font as SkFont +import org.jetbrains.skia.FontEdging as SkFontEdging import org.jetbrains.skia.FontFeature -import org.jetbrains.skia.FontStyle as SkFontStyle +import org.jetbrains.skia.FontHinting as SkFontHinting import org.jetbrains.skia.Paint as SkPaint import org.jetbrains.skia.PaintMode import org.jetbrains.skia.paragraph.Alignment as SkAlignment @@ -86,13 +82,26 @@ import org.jetbrains.skia.paragraph.TextBox import org.jetbrains.skia.paragraph.TextIndent as SkTextIndent import org.jetbrains.skia.paragraph.TextStyle as SkTextStyle +private fun FontSmoothing.toSkFontEdging(): SkFontEdging = when (this) { + FontSmoothing.None -> SkFontEdging.ALIAS + FontSmoothing.AntiAlias -> SkFontEdging.ANTI_ALIAS + FontSmoothing.SubpixelAntiAlias -> SkFontEdging.SUBPIXEL_ANTI_ALIAS +} + +private fun FontHinting.toSkFontHinting(): SkFontHinting = when (this) { + FontHinting.None -> SkFontHinting.NONE + FontHinting.Slight -> SkFontHinting.SLIGHT + FontHinting.Normal -> SkFontHinting.NORMAL + FontHinting.Full -> SkFontHinting.FULL +} + private val DefaultFontSize = 16.sp // Computed ComputedStyles always have font/letter size in pixels for particular `density`. // It's important because density could be changed in runtime, and it should force // SkTextStyle to be recalculated. Or we can have different densities in different windows. private sealed interface ComputedStyle { - val textForegroundStyle: TextForegroundStyle + val textForegroundStyle: ParagraphTextForegroundStyle val brushSize: Size val fontSize: Float val fontWeight: FontWeight? @@ -116,7 +125,7 @@ private sealed interface ComputedStyle { // Compile-time guarantee to be used as a key in the cache data class Immutable( - override val textForegroundStyle: TextForegroundStyle = TextForegroundStyle.Unspecified, + override val textForegroundStyle: ParagraphTextForegroundStyle = ParagraphTextForegroundStyle.Unspecified, override val brushSize: Size = Size.Unspecified, override val fontSize: Float = Float.NaN, override val fontWeight: FontWeight? = null, @@ -138,7 +147,7 @@ private sealed interface ComputedStyle { override val lineHeight: Float? = null, override val topRatio: Float = -1f, ) : ComputedStyle { - private val _foregroundPaint = SkiaTextPaint() + private val _foregroundPaint = ParagraphTextPaint() fun getForegroundPaint(): SkPaint { // `skiaPaint` doesn't create a copy, // so all the changes will be applied to skia paint. @@ -241,7 +250,7 @@ private sealed interface ComputedStyle { // Keep mutable variant to merge in place, without additional allocations class Mutable( - override var textForegroundStyle: TextForegroundStyle, + override var textForegroundStyle: ParagraphTextForegroundStyle, override var brushSize: Size, override var fontSize: Float, override var fontWeight: FontWeight?, @@ -265,7 +274,7 @@ private sealed interface ComputedStyle { ) : ComputedStyle { fun merge(density: Density, other: SpanStyle) { val fontSize = other.fontSize.toPx(density, fontSize) - textForegroundStyle = textForegroundStyle.merge(other.textForegroundStyle) + textForegroundStyle = textForegroundStyle.merge(other.toParagraphTextForegroundStyle()) other.fontFamily?.let { fontFamily = it } this.fontSize = fontSize other.fontWeight?.let { fontWeight = it } @@ -317,6 +326,7 @@ private sealed interface ComputedStyle { } } +@Suppress("INVISIBLE_REFERENCE") // FIXME: Make [Alignment.topRatio] public private fun ComputedStyle( density: Density, spanStyle: SpanStyle, @@ -326,7 +336,7 @@ private fun ComputedStyle( lineHeight: TextUnit, lineHeightStyle: LineHeightStyle?, ) = ComputedStyle.Mutable( - textForegroundStyle = spanStyle.textForegroundStyle, + textForegroundStyle = spanStyle.toParagraphTextForegroundStyle(), brushSize = brushSize, fontSize = with(density) { spanStyle.fontSize.toPx() }, fontWeight = spanStyle.fontWeight, @@ -383,7 +393,7 @@ internal class ParagraphBuilder( defaultStyle = ComputedStyle( density = density, spanStyle = initialStyle, - platformParagraphStyle = textStyle.paragraphStyle.platformStyle, + platformParagraphStyle = textStyle.toParagraphStyle().platformStyle, brushSize = brushSize, blendMode = blendMode, lineHeight = textStyle.lineHeight, @@ -422,12 +432,7 @@ internal class ParagraphBuilder( ps.ellipsis = ellipsis } - // this downcast is always safe because of sealed types, and we control construction - val platformFontLoader = (fontFamilyResolver as FontFamilyResolverImpl).platformFontLoader - val fontCollection = when (platformFontLoader) { - is SkiaFontLoader -> platformFontLoader.fontCollection - else -> throw IllegalStateException("Unsupported font loader $platformFontLoader") - } + val fontCollection = resolveParagraphFontCollection(fontFamilyResolver) val pb = SkParagraphBuilder(ps, fontCollection) @@ -535,10 +540,10 @@ internal class ParagraphBuilder( val cuts = mutableListOf() annotations.fastForEach { annotation -> // TODO https://youtrack.jetbrains.com/issue/CMP-7151 - if (annotation.item !is SpanStyle) return@fastForEach + val spanStyle = annotation.item as? SpanStyle ?: return@fastForEach - cuts.add(Cut.StyleAdd(annotation.start, annotation.item)) - cuts.add(Cut.StyleRemove(annotation.end, annotation.item)) + cuts.add(Cut.StyleAdd(annotation.start, spanStyle)) + cuts.add(Cut.StyleRemove(annotation.end, spanStyle)) } placeholders.fastForEach { placeholder -> @@ -597,7 +602,7 @@ internal class ParagraphBuilder( val style = ComputedStyle( density = density, spanStyle = activeStyles[0], - platformParagraphStyle = textStyle.paragraphStyle.platformStyle, + platformParagraphStyle = textStyle.toParagraphStyle().platformStyle, brushSize = brushSize, blendMode = blendMode, lineHeight = textStyle.lineHeight, @@ -755,26 +760,6 @@ private fun SpanStyle.copyWithDefaultFontSize(drawStyle: DrawStyle? = null): Spa ) } -// TODO: Remove from public -@InternalTextApi -fun FontStyle.toSkFontStyle(): SkFontStyle { - return when (this) { - FontStyle.Italic -> SkFontStyle.ITALIC - else -> SkFontStyle.NORMAL - } -} - -// TODO: Remove from public -@Suppress("unused") -@Deprecated( - message = "This method was not intended to be public", - level = DeprecationLevel.HIDDEN -) -@InternalTextApi -fun TextDecoration.toSkDecorationStyle(color: Color): SkDecorationStyle { - return toSkDecorationStyle(color, null) -} - private fun TextDecoration.toSkDecorationStyle( color: Color, textDecorationLineStyle: TextDecorationLineStyle? @@ -808,9 +793,7 @@ private fun TextDecorationLineStyle.toSkDecorationLineStyle(): SkDecorationLineS } } -// TODO: Remove from public -@InternalTextApi -fun PlaceholderVerticalAlign.toSkPlaceholderAlignment(): PlaceholderAlignment { +internal fun PlaceholderVerticalAlign.toSkPlaceholderAlignment(): PlaceholderAlignment { return when (this) { PlaceholderVerticalAlign.AboveBaseline -> PlaceholderAlignment.ABOVE_BASELINE PlaceholderVerticalAlign.TextTop -> PlaceholderAlignment.TOP diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsics.skiko.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkikoParagraphIntrinsics.nonAndroid.kt similarity index 86% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsics.skiko.kt rename to compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkikoParagraphIntrinsics.nonAndroid.kt index 77f1362227a2b..b529cef36d9b5 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsics.skiko.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkikoParagraphIntrinsics.nonAndroid.kt @@ -1,3 +1,5 @@ +@file:OptIn(InternalComposeUiApi::class) + /* * Copyright 2023 The Android Open Source Project * @@ -18,8 +20,14 @@ package androidx.compose.ui.text.platform import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.text.* +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString.Range +import androidx.compose.ui.text.ParagraphIntrinsics +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.StrongDirectionType +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.firstStrongDirectionType import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.LocaleList @@ -29,7 +37,7 @@ import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.Density import kotlin.math.ceil -internal class SkiaParagraphIntrinsics( +internal class SkikoParagraphIntrinsics( val text: String, private val style: TextStyle, private val annotations: List>, @@ -82,7 +90,9 @@ internal fun resolveTextDirection( return when (textDirection ?: TextDirection.Content) { TextDirection.Ltr -> ResolvedTextDirection.Ltr TextDirection.Rtl -> ResolvedTextDirection.Rtl - TextDirection.Content, TextDirection.Unspecified -> contentBasedTextDirection(text) { localeBasedTextDirection(localeList?.firstOrNull()) } + TextDirection.Content, TextDirection.Unspecified -> { + contentBasedTextDirection(text) { localeBasedTextDirection(localeList?.firstOrNull()) } + } TextDirection.ContentOrLtr -> contentBasedTextDirection(text) { ResolvedTextDirection.Ltr } TextDirection.ContentOrRtl -> contentBasedTextDirection(text) { ResolvedTextDirection.Rtl } else -> error("Invalid TextDirection.") diff --git a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/SkikoMatrixTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/SkikoMatrixTest.kt similarity index 100% rename from compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/SkikoMatrixTest.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/SkikoMatrixTest.kt diff --git a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/SkikoPathMeasureTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/SkikoPathMeasureTest.kt similarity index 91% rename from compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/SkikoPathMeasureTest.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/SkikoPathMeasureTest.kt index 1e15c0abc5add..2ebf04a046f24 100644 --- a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/SkikoPathMeasureTest.kt +++ b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/SkikoPathMeasureTest.kt @@ -16,6 +16,8 @@ package androidx.compose.ui.graphics +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runSkikoComposeUiTest import androidx.compose.ui.util.lerp import kotlin.test.Test import kotlin.test.assertContentEquals @@ -23,9 +25,9 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class SkikoPathMeasureTest { - @Test - fun getSegment_reusedDestinationUpdatesRetainedSkiaPath() { + @OptIn(ExperimentalTestApi::class) + fun getSegment_reusedDestinationUpdatesRetainedSkiaPath() = runSkikoComposeUiTest { val startX = 300f val startY = 450f val endX = 800f diff --git a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/SkikoPathTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/SkikoPathTest.kt similarity index 88% rename from compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/SkikoPathTest.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/SkikoPathTest.kt index 661a6fffcc653..67514baf5b372 100644 --- a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/SkikoPathTest.kt +++ b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/SkikoPathTest.kt @@ -16,7 +16,12 @@ package androidx.compose.ui.graphics +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.platform.clearSkikoComposeImplementation +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals @@ -24,8 +29,19 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue import org.jetbrains.skia.PathBuilder as SkPathBuilder +@OptIn(InternalComposeUiApi::class) class SkikoPathTest { + @BeforeTest + fun setup() { + registerSkikoComposeImplementation() + } + + @AfterTest + fun tearDown() { + clearSkikoComposeImplementation() + } + @Test fun asSkiaPath_observesSubsequentComposeMutations() { val path = Path().apply { diff --git a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayerTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayerTest.kt similarity index 99% rename from compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayerTest.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayerTest.kt index 486e3429979b2..8d3a8761447ce 100644 --- a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayerTest.kt +++ b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayerTest.kt @@ -36,6 +36,8 @@ import androidx.compose.ui.graphics.drawscope.inset import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.graphics.toPixelMap +import androidx.compose.ui.platform.clearSkikoComposeImplementation +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize @@ -45,6 +47,8 @@ import androidx.compose.ui.unit.toIntSize import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toSize import kotlin.math.roundToInt +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -55,6 +59,16 @@ import org.jetbrains.skia.Surface @OptIn(InternalComposeUiApi::class) class SkiaGraphicsLayerTest { + @BeforeTest + fun setup() { + registerSkikoComposeImplementation() + } + + @AfterTest + fun tearDown() { + clearSkikoComposeImplementation() + } + @Test fun testDrawLayer() { var layer: GraphicsLayer? = null @@ -1055,7 +1069,7 @@ class SkiaGraphicsLayerTest { verify?.invoke(imageBitmap.toPixelMap()) } finally { surface.close() - graphicsContext.dispose() + graphicsContext.close() } } diff --git a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/shadow/DropShadowPainterTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/shadow/DropShadowPainterTest.kt similarity index 92% rename from compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/shadow/DropShadowPainterTest.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/shadow/DropShadowPainterTest.kt index b19e2ae1abea1..ba900de84617e 100644 --- a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/shadow/DropShadowPainterTest.kt +++ b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/shadow/DropShadowPainterTest.kt @@ -24,6 +24,8 @@ import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runSkikoComposeUiTest import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -31,10 +33,11 @@ import kotlin.test.Test import kotlin.test.assertTrue // A copy from androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/shadow/DropShadowPainterTest.kt +@OptIn(ExperimentalTestApi::class) class DropShadowPainterTest { @Test - fun testDropShadowPainterWithColor() { + fun testDropShadowPainterWithColor() = runSkikoComposeUiTest { val dropShadow = DropShadowPainter(RectangleShape, Shadow(200.dp, Color.Red)) shadowTest( block = { @@ -54,7 +57,7 @@ class DropShadowPainterTest { } @Test - fun testDropShadowPainterWithPathAndColor() { + fun testDropShadowPainterWithPathAndColor() = runSkikoComposeUiTest { val dropShadow = DropShadowPainter( object : Shape { @@ -93,7 +96,7 @@ class DropShadowPainterTest { } @Test - fun testDropShadowPainterWithBrush() { + fun testDropShadowPainterWithBrush() = runSkikoComposeUiTest { val dropShadow = DropShadowPainter(RectangleShape, Shadow(200.dp, createTestImageShaderBrush())) shadowTest( @@ -114,7 +117,7 @@ class DropShadowPainterTest { } @Test - fun testDropShadowPainterPathWithBrush() { + fun testDropShadowPainterPathWithBrush() = runSkikoComposeUiTest { val dropShadow = DropShadowPainter( object : Shape { @@ -153,7 +156,7 @@ class DropShadowPainterTest { } @Test - fun testDropShadowPainterWithBrushAndColorFilter() { + fun testDropShadowPainterWithBrushAndColorFilter() = runSkikoComposeUiTest { val dropShadow = DropShadowPainter(RectangleShape, Shadow(200.dp, createTestImageShaderBrush())) shadowTest( @@ -174,7 +177,7 @@ class DropShadowPainterTest { } @Test - fun testDropShadowPainterWithBrushAndAlpha() { + fun testDropShadowPainterWithBrushAndAlpha() = runSkikoComposeUiTest { val dropShadow = DropShadowPainter( RectangleShape, diff --git a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/shadow/InnerShadowPainterTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/shadow/InnerShadowPainterTest.kt similarity index 89% rename from compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/shadow/InnerShadowPainterTest.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/shadow/InnerShadowPainterTest.kt index 0688567cf2ebe..58162779de121 100644 --- a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/shadow/InnerShadowPainterTest.kt +++ b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/shadow/InnerShadowPainterTest.kt @@ -18,15 +18,18 @@ package androidx.compose.ui.graphics.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runSkikoComposeUiTest import androidx.compose.ui.unit.dp import kotlin.test.Test import kotlin.test.assertTrue // A copy from androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/shadow/InnerShadowPainterTest.kt +@OptIn(ExperimentalTestApi::class) class InnerShadowPainterTest { @Test - fun testInnerShadowPainterWithColor() { + fun testInnerShadowPainterWithColor() = runSkikoComposeUiTest { val innerShadow = InnerShadowPainter(RectangleShape, Shadow(20.dp, Color.Red)) shadowTest( block = { @@ -46,7 +49,7 @@ class InnerShadowPainterTest { } @Test - fun testInnerShadowPainterWithPathAndColor() { + fun testInnerShadowPainterWithPathAndColor() = runSkikoComposeUiTest { val innerShadow = InnerShadowPainter(RectangleShape, Shadow(20.dp, Color.Red)) shadowTest( block = { diff --git a/compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/shadow/SkiaShadowTestHelper.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/shadow/SkiaShadowTestHelper.kt similarity index 100% rename from compose/ui/ui-graphics/src/skikoTest/kotlin/androidx/compose/ui/graphics/shadow/SkiaShadowTestHelper.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/graphics/shadow/SkiaShadowTestHelper.kt diff --git a/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/skiko/ImplementationRegistryTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/skiko/ImplementationRegistryTest.kt new file mode 100644 index 0000000000000..763e735d98406 --- /dev/null +++ b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/skiko/ImplementationRegistryTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.skiko + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.SkikoGraphicsImpl +import androidx.compose.ui.graphics.platform.PlatformGraphics +import androidx.compose.ui.graphics.platform.PlatformGraphicsRegistry +import androidx.compose.ui.platform.clearSkikoComposeImplementation +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import androidx.compose.ui.text.platform.PlatformText +import androidx.compose.ui.text.platform.PlatformTextRegistry +import androidx.compose.ui.text.SkikoTextImpl +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertFailsWith + +@OptIn(InternalComposeUiApi::class) +class ImplementationRegistryTest { + @AfterTest + fun tearDown() { + clearSkikoComposeImplementation() + } + + @Test + fun registerSkikoComposeImplementationIsIdempotent() { + PlatformGraphicsRegistry.clear() + PlatformTextRegistry.clear() + + registerSkikoComposeImplementation() + registerSkikoComposeImplementation() + + androidx.compose.ui.graphics.Paint() + androidx.compose.ui.text.font.createFontFamilyResolver() + } + + @Test + fun graphicsImplementationRejectsDifferentReplacement() { + PlatformTextRegistry.clear() + PlatformGraphicsRegistry.clear() + PlatformGraphicsRegistry.register(SkikoGraphicsImpl) + + val error = assertFailsWith { + PlatformGraphicsRegistry.register( + object : PlatformGraphics by SkikoGraphicsImpl {} + ) + } + + assertContains(error.message.orEmpty(), "already registered with a different instance") + } + + @Test + fun textImplementationRejectsDifferentReplacement() { + PlatformGraphicsRegistry.clear() + PlatformTextRegistry.clear() + PlatformTextRegistry.register(SkikoTextImpl) + + val error = assertFailsWith { + PlatformTextRegistry.register( + object : PlatformText by SkikoTextImpl {} + ) + } + + assertContains(error.message.orEmpty(), "already registered with a different instance") + } + + @Test + fun graphicsUsageFailsFastWithoutRegistration() { + PlatformGraphicsRegistry.clear() + + val error = assertFailsWith { + androidx.compose.ui.graphics.Paint() + } + + assertContains(error.message.orEmpty(), "No Compose UI graphics implementation is registered") + } + + @Test + fun textUsageFailsFastWithoutRegistration() { + PlatformTextRegistry.clear() + + val error = assertFailsWith { + androidx.compose.ui.text.font.createFontFamilyResolver() + } + + assertContains(error.message.orEmpty(), "No Compose UI text implementation is registered") + } +} diff --git a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/ExpireAfterAccessCacheTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/ExpireAfterAccessCacheTest.kt similarity index 100% rename from compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/ExpireAfterAccessCacheTest.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/ExpireAfterAccessCacheTest.kt diff --git a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/SkikoParagraphTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/SkikoParagraphTest.kt similarity index 96% rename from compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/SkikoParagraphTest.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/SkikoParagraphTest.kt index a8a672d685603..4305f12e6ba46 100644 --- a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/SkikoParagraphTest.kt +++ b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/SkikoParagraphTest.kt @@ -16,7 +16,10 @@ package androidx.compose.ui.text +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.clearSkikoComposeImplementation +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection @@ -25,6 +28,8 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals @@ -34,8 +39,21 @@ import kotlinx.test.IgnoreJsTarget import kotlinx.test.IgnoreWasmTarget // Adopted tests from text/text/src/androidTest/java/androidx/compose/ui/text/android/selection/WordBoundaryTest.kt +@OptIn(InternalComposeUiApi::class) class SkikoParagraphTest { - private val fontFamilyResolver = createFontFamilyResolver() + @BeforeTest + fun setup() { + registerSkikoComposeImplementation() + } + + @AfterTest + fun tearDown() { + clearSkikoComposeImplementation() + } + + // Lazy so the registry is populated by [setup] before the resolver is created (the property + // initializer would otherwise run at construction time, before @BeforeTest). + private val fontFamilyResolver by lazy { createFontFamilyResolver() } private val defaultDensity = Density(density = 1f) private val maxWidthConstraint = 1000 diff --git a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/StringTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/StringTest.kt similarity index 99% rename from compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/StringTest.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/StringTest.kt index e48f76612d785..2c8f29a54d8d9 100644 --- a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/StringTest.kt +++ b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/StringTest.kt @@ -65,4 +65,4 @@ class StringTest { assertEquals(StrongDirectionType.Rtl, 'א'.code.strongDirectionType()) // Hebrew assertEquals(StrongDirectionType.Rtl, '؈'.code.strongDirectionType()) // Arabic } -} \ No newline at end of file +} diff --git a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsicsTest.kt b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsicsTest.kt similarity index 98% rename from compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsicsTest.kt rename to compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsicsTest.kt index 7afeada990d0e..e96ba0cd34890 100644 --- a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsicsTest.kt +++ b/compose/ui/ui-skiko/src/nonAndroidTest/kotlin/androidx/compose/ui/text/platform/SkiaParagraphIntrinsicsTest.kt @@ -22,8 +22,6 @@ import androidx.compose.ui.text.style.ResolvedTextDirection import androidx.compose.ui.text.style.TextDirection import kotlin.test.Test import kotlin.test.assertEquals -import kotlinx.test.IgnoreJsTarget -import kotlinx.test.IgnoreWasmTarget class SkiaParagraphIntrinsicsTest { @@ -112,4 +110,4 @@ class SkiaParagraphIntrinsicsTest { assertEquals(ResolvedTextDirection.Ltr, resolveTextDirection(text, TextDirection.ContentOrLtr)) assertEquals(ResolvedTextDirection.Ltr, resolveTextDirection(text, TextDirection.ContentOrRtl)) } -} \ No newline at end of file +} diff --git a/compose/ui/ui-text/src/nonJvmMain/kotlin/androidx/compose/ui/text/CharHelpers.nonJvm.kt b/compose/ui/ui-skiko/src/nonJvmMain/kotlin/androidx/compose/ui/text/CharHelpers.nonJvm.kt similarity index 99% rename from compose/ui/ui-text/src/nonJvmMain/kotlin/androidx/compose/ui/text/CharHelpers.nonJvm.kt rename to compose/ui/ui-skiko/src/nonJvmMain/kotlin/androidx/compose/ui/text/CharHelpers.nonJvm.kt index 17bbd81e725cf..1ad50f0327b28 100644 --- a/compose/ui/ui-text/src/nonJvmMain/kotlin/androidx/compose/ui/text/CharHelpers.nonJvm.kt +++ b/compose/ui/ui-skiko/src/nonJvmMain/kotlin/androidx/compose/ui/text/CharHelpers.nonJvm.kt @@ -17,7 +17,6 @@ package androidx.compose.ui.text import org.jetbrains.skia.icu.CharDirection - /** * Get strong (R, L or AL) direction type. * See https://www.unicode.org/reports/tr9/ @@ -39,4 +38,4 @@ internal actual fun CodePoint.isNeutralDirection(): Boolean = CharDirection.BOUNDARY_NEUTRAL -> true else -> false - } \ No newline at end of file + } diff --git a/compose/ui/ui-graphics/src/webMain/kotlin/androidx/compose/ui/graphics/Actuals.web.kt b/compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/graphics/Actuals.web.kt similarity index 96% rename from compose/ui/ui-graphics/src/webMain/kotlin/androidx/compose/ui/graphics/Actuals.web.kt rename to compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/graphics/Actuals.web.kt index 493edf7555cd4..485e3aa3d5a4c 100644 --- a/compose/ui/ui-graphics/src/webMain/kotlin/androidx/compose/ui/graphics/Actuals.web.kt +++ b/compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/graphics/Actuals.web.kt @@ -17,9 +17,7 @@ package androidx.compose.ui.graphics import org.jetbrains.skia.Bitmap -import org.jetbrains.skia.ColorAlphaType import org.jetbrains.skia.Image -import org.jetbrains.skia.ImageInfo /* The default implementation (used for all but web) based on `canvas.drawImage` call is slow for web: @@ -55,4 +53,4 @@ internal actual fun Image.toBitmap(): Bitmap { val bitmap = Bitmap.makeFromImage(this) bitmap.setImmutable() return bitmap -} \ No newline at end of file +} diff --git a/compose/ui/ui-graphics/src/webMain/kotlin/androidx/compose/ui/graphics/ImageAsset.web.kt b/compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/graphics/ImageAsset.web.kt similarity index 52% rename from compose/ui/ui-graphics/src/webMain/kotlin/androidx/compose/ui/graphics/ImageAsset.web.kt rename to compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/graphics/ImageAsset.web.kt index 2bd3fe215d52f..205588b0b3562 100644 --- a/compose/ui/ui-graphics/src/webMain/kotlin/androidx/compose/ui/graphics/ImageAsset.web.kt +++ b/compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/graphics/ImageAsset.web.kt @@ -17,20 +17,22 @@ package androidx.compose.ui.graphics internal actual fun ByteArray.putBytesInto(array: IntArray, offset: Int, length: Int) { - if (offset < 0 || length < 0 || offset + length > array.size) { - throw IndexOutOfBoundsException("Invalid offset or length") - } + if (offset < 0 || length < 0 || offset + length > array.size) { + throw IndexOutOfBoundsException("Invalid offset or length") + } - if (length * 4 > this.size) { - throw IndexOutOfBoundsException("ByteArray not big enough to hold the requested number of integers") - } + if (length * 4 > this.size) { + throw IndexOutOfBoundsException( + "ByteArray not big enough to hold the requested number of integers" + ) + } - for (i in 0 until length) { - val byteIndex = i * 4 - array[offset + i] = (this[byteIndex].toInt() and 0xFF) or - ((this[byteIndex + 1].toInt() and 0xFF) shl 8) or - ((this[byteIndex + 2].toInt() and 0xFF) shl 16) or - ((this[byteIndex + 3].toInt() and 0xFF) shl 24) - } + for (index in 0 until length) { + val byteIndex = index * 4 + array[offset + index] = + (this[byteIndex].toInt() and 0xFF) or + ((this[byteIndex + 1].toInt() and 0xFF) shl 8) or + ((this[byteIndex + 2].toInt() and 0xFF) shl 16) or + ((this[byteIndex + 3].toInt() and 0xFF) shl 24) + } } - diff --git a/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/Cache.web.kt b/compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/text/Cache.web.kt similarity index 87% rename from compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/Cache.web.kt rename to compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/text/Cache.web.kt index 984b810c263bb..a65a3af9ce9c6 100644 --- a/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/Cache.web.kt +++ b/compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/text/Cache.web.kt @@ -81,4 +81,17 @@ internal actual class WeakKeysCache { @OptIn(ExperimentalWasmJsInterop::class) internal external class FinalizationRegistry(cleanup: (JsAny) -> Unit) { fun register(target: JsAny, heldValue: JsAny) +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef +@OptIn(ExperimentalWasmJsInterop::class) +private external class WeakRef { + constructor(target: JsAny) + fun deref(): JsAny? +} + +@OptIn(ExperimentalWasmJsInterop::class) +private class WeakReference(reference: T) { + private val weakRef = WeakRef(reference.toJsReference()) + fun get(): T? = weakRef.deref()?.unsafeCast>()?.get() } \ No newline at end of file diff --git a/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.web.kt b/compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.web.kt similarity index 100% rename from compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.web.kt rename to compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/text/platform/ParagraphLayouter.web.kt diff --git a/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/platform/WebFont.kt b/compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/text/platform/WebFont.web.kt similarity index 94% rename from compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/platform/WebFont.kt rename to compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/text/platform/WebFont.web.kt index 9a2d0360acf87..26b63c831309c 100644 --- a/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/platform/WebFont.kt +++ b/compose/ui/ui-skiko/src/webMain/kotlin/androidx/compose/ui/text/platform/WebFont.web.kt @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package androidx.compose.ui.text.platform import org.jetbrains.skia.FontStyle as SkFontStyle import org.jetbrains.skia.Typeface as SkTypeface +import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontStyle import org.jetbrains.skia.Data @@ -26,12 +28,13 @@ import org.jetbrains.skia.FontWidth import org.jetbrains.skiko.OS import org.jetbrains.skiko.hostOs +@OptIn(ExperimentalTextApi::class) internal actual fun loadTypeface(font: Font): SkTypeface { if (font !is PlatformFont) { throw IllegalArgumentException("Unsupported font type: $font") } return when (font) { - is LoadedFont -> FontMgr.default.makeFromData(Data.makeFromBytes(font.getData())) + is LoadedFont -> FontMgr.default.makeFromData(Data.makeFromBytes(font.data)) ?: error("loadTypeface makeFromData failed") is SystemFont -> FontMgr.default.legacyMakeTypeface(font.identity, font.skFontStyle) diff --git a/compose/ui/ui-text/src/webTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt b/compose/ui/ui-skiko/src/webTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt similarity index 100% rename from compose/ui/ui-text/src/webTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt rename to compose/ui/ui-skiko/src/webTest/kotlin/androidx/compose/ui/text/WeakKeysCacheTest.kt diff --git a/compose/ui/ui-skiko/src/webTest/kotlin/kotlinx/test/IgnoreTargets.web.kt b/compose/ui/ui-skiko/src/webTest/kotlin/kotlinx/test/IgnoreTargets.web.kt new file mode 100644 index 0000000000000..427968f43c8c8 --- /dev/null +++ b/compose/ui/ui-skiko/src/webTest/kotlin/kotlinx/test/IgnoreTargets.web.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kotlinx.test + +actual typealias IgnoreJsTarget = kotlin.test.Ignore + +actual typealias IgnoreWasmTarget = kotlin.test.Ignore diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle index 4f11d9fb05ad6..a76fc374219bb 100644 --- a/compose/ui/ui-test-junit4/build.gradle +++ b/compose/ui/ui-test-junit4/build.gradle @@ -119,6 +119,7 @@ androidXMultiplatform { dependencies { implementation(libs.truth) implementation(libs.skiko) + implementation(project(":compose:ui:ui-skiko")) } } diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt index 542472af87b7c..ba203959597f1 100644 --- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt +++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt @@ -44,14 +44,15 @@ import org.junit.runners.model.Statement "coroutines are queued rather than executed immediately.", level = DeprecationLevel.WARNING, ) -@OptIn(InternalTestApi::class, ExperimentalTestApi::class) -actual fun createComposeRule(): ComposeContentTestRule = - DesktopComposeTestRule( +@OptIn(InternalTestApi::class, ExperimentalTestApi::class, InternalComposeUiApi::class) +actual fun createComposeRule(): ComposeContentTestRule { + return DesktopComposeTestRule( DesktopComposeUiTest( effectContext = EmptyCoroutineContext, useStandardTestDispatcherForComposition = false ) ) +} @Deprecated( message = @@ -61,14 +62,15 @@ actual fun createComposeRule(): ComposeContentTestRule = level = DeprecationLevel.WARNING, ) @ExperimentalTestApi -@OptIn(InternalTestApi::class) -actual fun createComposeRule(effectContext: CoroutineContext): ComposeContentTestRule = - DesktopComposeTestRule( +@OptIn(InternalTestApi::class, InternalComposeUiApi::class) +actual fun createComposeRule(effectContext: CoroutineContext): ComposeContentTestRule { + return DesktopComposeTestRule( DesktopComposeUiTest( effectContext = effectContext, useStandardTestDispatcherForComposition = false ) ) +} @InternalTestApi @OptIn(ExperimentalTestApi::class) diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopScreenshotTestRule.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopScreenshotTestRule.desktop.kt index 98bb47ac088e4..9e9069f70aa01 100644 --- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopScreenshotTestRule.desktop.kt +++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopScreenshotTestRule.desktop.kt @@ -16,6 +16,8 @@ package androidx.compose.ui.test.junit4 import androidx.annotation.FloatRange +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.test.InternalTestApi import androidx.compose.ui.test.junit4.ScreenshotResultProto.Status import androidx.compose.ui.test.junit4.matchers.BitmapMatcher @@ -57,10 +59,14 @@ private data class ScreenshotResultProto( } @InternalTestApi +@OptIn(InternalComposeUiApi::class) fun DesktopScreenshotTestRule( modulePath: String, fsGoldenPath: String = System.getProperty("GOLDEN_PATH") ): ScreenshotTestRule { + // Register here (at rule construction) so the backend is available before tests that create + // graphics primitives in field initializers — e.g. DesktopGraphicsTest's paint properties. + registerSkikoComposeImplementation() return ScreenshotTestRule(fsGoldenPath, modulePath) } @@ -77,6 +83,8 @@ class ScreenshotTestRule internal constructor( override fun evaluate() { testIdentifier = "${description!!.className}_${description.methodName}" .replace(".", "_").replace(",", "_").replace(" ", "_").replace("__", "_") + // TODO: re-enable cleanup (see ComposeUiTest.skiko.kt) once async-registration tests + // no longer rely on the backend registration persisting across tests. base.evaluate() } } diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle index f9e0101bd5ce1..be595c2c250ff 100644 --- a/compose/ui/ui-test/build.gradle +++ b/compose/ui/ui-test/build.gradle @@ -125,6 +125,7 @@ androidXMultiplatform { dependsOn(commonMain) dependencies { implementation(libs.atomicFu) + implementation(project(":compose:ui:ui-skiko")) // Required to properly resolve supertypes of DefaultArchitectureComponentsOwner // Keep in sync with :ui:ui module diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt index 46ca38b1cdd76..23d83e6d7ee34 100644 --- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt +++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt @@ -16,6 +16,7 @@ package androidx.compose.ui.test +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.unit.Density import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -38,6 +39,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi level = DeprecationLevel.WARNING, ) @ExperimentalTestApi +@OptIn(InternalComposeUiApi::class) fun runDesktopComposeUiTest( width: Int = 1024, height: Int = 768, diff --git a/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt b/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt index 656c18fcb0667..16e4e9ec572b9 100644 --- a/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt +++ b/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.platform.PlatformDragAndDropSource import androidx.compose.ui.platform.PlatformTextInputMethodRequest import androidx.compose.ui.platform.PlatformWindowInsets import androidx.compose.ui.platform.WindowInfo +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.scene.CanvasLayersComposeScene import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.semantics.SemanticsNode @@ -283,6 +284,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor( } private inline fun withScene(block: () -> R): R { + registerSkikoComposeImplementation() runOnUiThread(::createScene) try { return block() @@ -291,6 +293,10 @@ open class SkikoComposeUiTest @InternalTestApi constructor( // After the scene is closed, run all left foreground TestDispatchEvent. // They might've been added outside the runTest call, using the provided coroutineDispatcher: compositionCoroutineDispatcher.scheduler.advanceUntilIdle() + // TODO: re-enable once tests that register the backend asynchronously (e.g. AWT + // ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on the + // registration persisting across tests. + // clearSkikoComposeImplementation() uncaughtExceptionHandler.throwUncaught() } } diff --git a/compose/ui/ui-text/api/desktop/ui-text.api b/compose/ui/ui-text/api/desktop/ui-text.api index b0d7dbb2f4151..7ed391d76e8c8 100644 --- a/compose/ui/ui-text/api/desktop/ui-text.api +++ b/compose/ui/ui-text/api/desktop/ui-text.api @@ -1499,17 +1499,6 @@ public final class androidx/compose/ui/text/intl/LocaleList$Companion { public final fun getEmpty ()Landroidx/compose/ui/text/intl/LocaleList; } -public final class androidx/compose/ui/text/platform/DesktopFont_desktopKt { - public static final fun Font-Ej4NQ78 (Ljava/io/File;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;)Landroidx/compose/ui/text/font/Font; - public static final fun Font-Ej4NQ78 (Ljava/lang/String;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;)Landroidx/compose/ui/text/font/Font; - public static synthetic fun Font-Ej4NQ78$default (Ljava/io/File;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;ILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; - public static synthetic fun Font-Ej4NQ78$default (Ljava/lang/String;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;ILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; - public static final fun Font-RetOiIg (Ljava/io/File;Landroidx/compose/ui/text/font/FontWeight;I)Landroidx/compose/ui/text/font/Font; - public static final fun Font-RetOiIg (Ljava/lang/String;Landroidx/compose/ui/text/font/FontWeight;I)Landroidx/compose/ui/text/font/Font; - public static synthetic fun Font-RetOiIg$default (Ljava/io/File;Landroidx/compose/ui/text/font/FontWeight;IILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; - public static synthetic fun Font-RetOiIg$default (Ljava/lang/String;Landroidx/compose/ui/text/font/FontWeight;IILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; -} - public final class androidx/compose/ui/text/platform/FileFont : androidx/compose/ui/text/platform/PlatformFont { public static final field $stable I public synthetic fun (Ljava/io/File;Landroidx/compose/ui/text/font/FontWeight;IILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -1525,20 +1514,6 @@ public final class androidx/compose/ui/text/platform/FileFont : androidx/compose public fun toString ()Ljava/lang/String; } -public final class androidx/compose/ui/text/platform/FontLoadResult { - public static final field $stable I - public fun (Lorg/jetbrains/skia/Typeface;Ljava/util/List;)V - public final fun getAliases ()Ljava/util/List; - public final fun getTypeface ()Lorg/jetbrains/skia/Typeface; -} - -public final class androidx/compose/ui/text/platform/FontLoader : androidx/compose/ui/text/font/Font$ResourceLoader { - public static final field $stable I - public fun ()V - public synthetic fun load (Landroidx/compose/ui/text/font/Font;)Ljava/lang/Object; - public fun load (Landroidx/compose/ui/text/font/Font;)Lorg/jetbrains/skia/Typeface; -} - public final class androidx/compose/ui/text/platform/LoadedFont : androidx/compose/ui/text/platform/PlatformFont { public static final field $stable I public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -1559,19 +1534,6 @@ public abstract class androidx/compose/ui/text/platform/PlatformFont : androidx/ public abstract fun getVariationSettings ()Landroidx/compose/ui/text/font/FontVariation$Settings; } -public final class androidx/compose/ui/text/platform/PlatformFont_skikoKt { - public static final fun Font-MuC2MFs (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;)Landroidx/compose/ui/text/font/Font; - public static final fun Font-MuC2MFs (Ljava/lang/String;[BLandroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;)Landroidx/compose/ui/text/font/Font; - public static synthetic fun Font-MuC2MFs$default (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;ILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; - public static synthetic fun Font-MuC2MFs$default (Ljava/lang/String;[BLandroidx/compose/ui/text/font/FontWeight;ILandroidx/compose/ui/text/font/FontVariation$Settings;ILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; - public static final fun Font-wCLgNak (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/text/font/FontWeight;I)Landroidx/compose/ui/text/font/Font; - public static final fun Font-wCLgNak (Ljava/lang/String;[BLandroidx/compose/ui/text/font/FontWeight;I)Landroidx/compose/ui/text/font/Font; - public static synthetic fun Font-wCLgNak$default (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/text/font/FontWeight;IILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; - public static synthetic fun Font-wCLgNak$default (Ljava/lang/String;[BLandroidx/compose/ui/text/font/FontWeight;IILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; - public static final fun Typeface (Lorg/jetbrains/skia/Typeface;Ljava/lang/String;)Landroidx/compose/ui/text/font/Typeface; - public static synthetic fun Typeface$default (Lorg/jetbrains/skia/Typeface;Ljava/lang/String;ILjava/lang/Object;)Landroidx/compose/ui/text/font/Typeface; -} - public final class androidx/compose/ui/text/platform/ResourceFont : androidx/compose/ui/text/platform/PlatformFont { public static final field $stable I public synthetic fun (Ljava/lang/String;Landroidx/compose/ui/text/font/FontWeight;IILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/compose/ui/ui-text/api/ui-text.klib.api b/compose/ui/ui-text/api/ui-text.klib.api index d06e55c346a8b..c56e9bb9f6491 100644 --- a/compose/ui/ui-text/api/ui-text.klib.api +++ b/compose/ui/ui-text/api/ui-text.klib.api @@ -598,21 +598,6 @@ final class androidx.compose.ui.text.intl/LocaleList : kotlin.collections/Collec } } -final class androidx.compose.ui.text.platform/FontLoadResult { // androidx.compose.ui.text.platform/FontLoadResult|null[0] - constructor (org.jetbrains.skia/Typeface?, kotlin.collections/List) // androidx.compose.ui.text.platform/FontLoadResult.|(org.jetbrains.skia.Typeface?;kotlin.collections.List){}[0] - - final val aliases // androidx.compose.ui.text.platform/FontLoadResult.aliases|{}aliases[0] - final fun (): kotlin.collections/List // androidx.compose.ui.text.platform/FontLoadResult.aliases.|(){}[0] - final val typeface // androidx.compose.ui.text.platform/FontLoadResult.typeface|{}typeface[0] - final fun (): org.jetbrains.skia/Typeface? // androidx.compose.ui.text.platform/FontLoadResult.typeface.|(){}[0] -} - -final class androidx.compose.ui.text.platform/FontLoader : androidx.compose.ui.text.font/Font.ResourceLoader { // androidx.compose.ui.text.platform/FontLoader|null[0] - constructor () // androidx.compose.ui.text.platform/FontLoader.|(){}[0] - - final fun load(androidx.compose.ui.text.font/Font): org.jetbrains.skia/Typeface // androidx.compose.ui.text.platform/FontLoader.load|load(androidx.compose.ui.text.font.Font){}[0] -} - final class androidx.compose.ui.text.platform/LoadedFont : androidx.compose.ui.text.platform/PlatformFont { // androidx.compose.ui.text.platform/LoadedFont|null[0] constructor (kotlin/String, kotlin/Function0, androidx.compose.ui.text.font/FontWeight, androidx.compose.ui.text.font/FontStyle) // androidx.compose.ui.text.platform/LoadedFont.|(kotlin.String;kotlin.Function0;androidx.compose.ui.text.font.FontWeight;androidx.compose.ui.text.font.FontStyle){}[0] @@ -1847,19 +1832,9 @@ final val androidx.compose.ui.text.input/androidx_compose_ui_text_input_TextInpu final val androidx.compose.ui.text.input/androidx_compose_ui_text_input_TransformedText$stableprop // androidx.compose.ui.text.input/androidx_compose_ui_text_input_TransformedText$stableprop|#static{}androidx_compose_ui_text_input_TransformedText$stableprop[0] final val androidx.compose.ui.text.intl/androidx_compose_ui_text_intl_Locale$stableprop // androidx.compose.ui.text.intl/androidx_compose_ui_text_intl_Locale$stableprop|#static{}androidx_compose_ui_text_intl_Locale$stableprop[0] final val androidx.compose.ui.text.intl/androidx_compose_ui_text_intl_LocaleList$stableprop // androidx.compose.ui.text.intl/androidx_compose_ui_text_intl_LocaleList$stableprop|#static{}androidx_compose_ui_text_intl_LocaleList$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop|#static{}androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop|#static{}androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoadResult$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoadResult$stableprop|#static{}androidx_compose_ui_text_platform_FontLoadResult$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoader$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoader$stableprop|#static{}androidx_compose_ui_text_platform_FontLoader$stableprop[0] final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_LoadedFont$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_LoadedFont$stableprop|#static{}androidx_compose_ui_text_platform_LoadedFont$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop[0] -final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop|#static{}androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop[0] final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_PlatformFont$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_PlatformFont$stableprop|#static{}androidx_compose_ui_text_platform_PlatformFont$stableprop[0] +final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_PlatformTextRegistry$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_PlatformTextRegistry$stableprop|#static{}androidx_compose_ui_text_platform_PlatformTextRegistry$stableprop[0] final val androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_SystemFont$stableprop // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_SystemFont$stableprop|#static{}androidx_compose_ui_text_platform_SystemFont$stableprop[0] final val androidx.compose.ui.text.style/androidx_compose_ui_text_style_LineHeightStyle$stableprop // androidx.compose.ui.text.style/androidx_compose_ui_text_style_LineHeightStyle$stableprop|#static{}androidx_compose_ui_text_style_LineHeightStyle$stableprop[0] final val androidx.compose.ui.text.style/androidx_compose_ui_text_style_TextDecoration$stableprop // androidx.compose.ui.text.style/androidx_compose_ui_text_style_TextDecoration$stableprop|#static{}androidx_compose_ui_text_style_TextDecoration$stableprop[0] @@ -1969,24 +1944,9 @@ final fun androidx.compose.ui.text.input/androidx_compose_ui_text_input_TextInpu final fun androidx.compose.ui.text.input/androidx_compose_ui_text_input_TransformedText$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.input/androidx_compose_ui_text_input_TransformedText$stableprop_getter|androidx_compose_ui_text_input_TransformedText$stableprop_getter(){}[0] final fun androidx.compose.ui.text.intl/androidx_compose_ui_text_intl_Locale$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.intl/androidx_compose_ui_text_intl_Locale$stableprop_getter|androidx_compose_ui_text_intl_Locale$stableprop_getter(){}[0] final fun androidx.compose.ui.text.intl/androidx_compose_ui_text_intl_LocaleList$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.intl/androidx_compose_ui_text_intl_LocaleList$stableprop_getter|androidx_compose_ui_text_intl_LocaleList$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/Font(kotlin/String, kotlin/ByteArray, androidx.compose.ui.text.font/FontWeight = ..., androidx.compose.ui.text.font/FontStyle = ...): androidx.compose.ui.text.font/Font // androidx.compose.ui.text.platform/Font|Font(kotlin.String;kotlin.ByteArray;androidx.compose.ui.text.font.FontWeight;androidx.compose.ui.text.font.FontStyle){}[0] -final fun androidx.compose.ui.text.platform/Font(kotlin/String, kotlin/ByteArray, androidx.compose.ui.text.font/FontWeight = ..., androidx.compose.ui.text.font/FontStyle = ..., androidx.compose.ui.text.font/FontVariation.Settings = ...): androidx.compose.ui.text.font/Font // androidx.compose.ui.text.platform/Font|Font(kotlin.String;kotlin.ByteArray;androidx.compose.ui.text.font.FontWeight;androidx.compose.ui.text.font.FontStyle;androidx.compose.ui.text.font.FontVariation.Settings){}[0] -final fun androidx.compose.ui.text.platform/Font(kotlin/String, kotlin/Function0, androidx.compose.ui.text.font/FontWeight = ..., androidx.compose.ui.text.font/FontStyle = ...): androidx.compose.ui.text.font/Font // androidx.compose.ui.text.platform/Font|Font(kotlin.String;kotlin.Function0;androidx.compose.ui.text.font.FontWeight;androidx.compose.ui.text.font.FontStyle){}[0] -final fun androidx.compose.ui.text.platform/Font(kotlin/String, kotlin/Function0, androidx.compose.ui.text.font/FontWeight = ..., androidx.compose.ui.text.font/FontStyle = ..., androidx.compose.ui.text.font/FontVariation.Settings = ...): androidx.compose.ui.text.font/Font // androidx.compose.ui.text.platform/Font|Font(kotlin.String;kotlin.Function0;androidx.compose.ui.text.font.FontWeight;androidx.compose.ui.text.font.FontStyle;androidx.compose.ui.text.font.FontVariation.Settings){}[0] -final fun androidx.compose.ui.text.platform/Typeface(org.jetbrains.skia/Typeface, kotlin/String? = ...): androidx.compose.ui.text.font/Typeface // androidx.compose.ui.text.platform/Typeface|Typeface(org.jetbrains.skia.Typeface;kotlin.String?){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop_getter|androidx_compose_ui_text_platform_ComputedStyle_Immutable$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop_getter|androidx_compose_ui_text_platform_ComputedStyle_Mutable$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoadResult$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoadResult$stableprop_getter|androidx_compose_ui_text_platform_FontLoadResult$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoader$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_FontLoader$stableprop_getter|androidx_compose_ui_text_platform_FontLoader$stableprop_getter(){}[0] final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_LoadedFont$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_LoadedFont$stableprop_getter|androidx_compose_ui_text_platform_LoadedFont$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Cut_EndPlaceholder$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Cut_PutPlaceholder$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleAdd$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Cut_StyleRemove$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Op_EndPlaceholder$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Op_PutPlaceholder$stableprop_getter(){}[0] -final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop_getter|androidx_compose_ui_text_platform_ParagraphBuilder_Op_StyleAdd$stableprop_getter(){}[0] final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_PlatformFont$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_PlatformFont$stableprop_getter|androidx_compose_ui_text_platform_PlatformFont$stableprop_getter(){}[0] +final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_PlatformTextRegistry$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_PlatformTextRegistry$stableprop_getter|androidx_compose_ui_text_platform_PlatformTextRegistry$stableprop_getter(){}[0] final fun androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_SystemFont$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.platform/androidx_compose_ui_text_platform_SystemFont$stableprop_getter|androidx_compose_ui_text_platform_SystemFont$stableprop_getter(){}[0] final fun androidx.compose.ui.text.style/androidx_compose_ui_text_style_LineHeightStyle$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.style/androidx_compose_ui_text_style_LineHeightStyle$stableprop_getter|androidx_compose_ui_text_style_LineHeightStyle$stableprop_getter(){}[0] final fun androidx.compose.ui.text.style/androidx_compose_ui_text_style_TextDecoration$stableprop_getter(): kotlin/Int // androidx.compose.ui.text.style/androidx_compose_ui_text_style_TextDecoration$stableprop_getter|androidx_compose_ui_text_style_TextDecoration$stableprop_getter(){}[0] diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle index a140514f3852b..bc66465624d10 100644 --- a/compose/ui/ui-text/build.gradle +++ b/compose/ui/ui-text/build.gradle @@ -111,28 +111,28 @@ androidXMultiplatform { implementation(libs.byteBuddy) } - // TODO: Align naming: nonAndroidMain - skikoMain { + nonAndroidMain { dependsOn(commonMain) dependencies { - api(libs.skiko) + // ui-text is skia-free: the skia-backed font/paragraph backend lives in ui-skiko. implementation(libs.atomicFu) } } - skikoTest { + nonAndroidTest { dependsOn(commonTest) dependencies { + implementation(project(":compose:ui:ui-skiko")) implementation(project(":kruth:kruth")) } } desktopMain { - dependsOn(skikoMain) + dependsOn(nonAndroidMain) } desktopTest { - dependsOn(skikoTest) + dependsOn(nonAndroidTest) dependencies { implementation(libs.truth) implementation(libs.junit) @@ -149,7 +149,7 @@ androidXMultiplatform { } nonJvmMain { - dependsOn(skikoMain) + dependsOn(nonAndroidMain) dependencies { // To comply with Klib resolver until https://youtrack.jetbrains.com/issue/KT-61096 is fixed implementation("org.jetbrains.compose.collection-internal:collection:1.10.0") @@ -157,7 +157,7 @@ androidXMultiplatform { } nonJvmTest { - dependsOn(skikoTest) + dependsOn(nonAndroidTest) } nativeMain { @@ -192,13 +192,6 @@ androidXMultiplatform { dependsOn(darwinTest) } - wasmJsMain { - dependencies { - implementation(libs.skikoWasmJs) - implementation(libs.skikoJsWasmRuntime) - } - } - // TODO: Align it with AOSP or make explicit configureEach { languageSettings.optIn("androidx.compose.ui.text.ExperimentalTextApi") diff --git a/compose/ui/ui-text/src/darwinMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.darwin.kt b/compose/ui/ui-text/src/darwinMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.darwin.kt index 7219c32f120b7..d621ade3dbaa2 100644 --- a/compose/ui/ui-text/src/darwinMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.darwin.kt +++ b/compose/ui/ui-text/src/darwinMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.darwin.kt @@ -17,6 +17,7 @@ package androidx.compose.ui.text.intl import androidx.compose.runtime.Immutable +import androidx.compose.ui.InternalComposeUiApi import platform.Foundation.* @Deprecated( @@ -77,6 +78,7 @@ fun NSLocale.toComposeLocale(): Locale = Locale(this) private fun NSLocale.isRtl(): Boolean = NSLocale.characterDirectionForLanguage(languageCode) == NSLocaleLanguageDirectionRightToLeft -internal actual fun Locale.isRtl(): Boolean { +@InternalComposeUiApi +actual fun Locale.isRtl(): Boolean { return platformLocale.isRtl() } \ No newline at end of file diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/font/FontFamily.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/font/FontFamily.desktop.kt index bb4b292d75c4c..604acb2af6a24 100644 --- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/font/FontFamily.desktop.kt +++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/font/FontFamily.desktop.kt @@ -18,27 +18,22 @@ package androidx.compose.ui.text.font import androidx.compose.runtime.Stable import androidx.compose.ui.text.ExperimentalTextApi -import androidx.compose.ui.text.platform.Font -import androidx.compose.ui.text.platform.JetBrainsRuntimeFontFamilies import androidx.compose.ui.text.platform.SystemFont -import androidx.compose.ui.text.platform.asComposeFontFamily -import java.awt.Font /** * Load a [FontFamily] from a system font family name. If the [familyName] * doesn't match any available family in the system, the lookup will return * a fallback font family. * - * If you're trying to use an AWT [Font] in Compose, use the - * [Font.asComposeFontFamily] function instead, which will take care of - * some AWT-specific quirks, too. If you want to load a font family - * embedded in the JetBrains Runtime, you can use [EmbeddedFontFamily]. + * If you're trying to use an AWT `java.awt.Font` in Compose, use the + * `Font.asComposeFontFamily` function instead (in ui-skiko), which will take + * care of some AWT-specific quirks, too. If you want to load a font family + * embedded in the JetBrains Runtime, you can use `EmbeddedFontFamily` (in + * ui-skiko). * * @param familyName The name of the system font family to load. * @return the requested system font family, or a fallback if [familyName] * doesn't match any available system font family. - * @see Font.asComposeFontFamily - * @see EmbeddedFontFamily */ @ExperimentalTextApi @Stable @@ -65,27 +60,3 @@ fun FontFamily(familyName: String): FontFamily = SystemFont(familyName, FontWeight.W900, FontStyle.Italic), ) ) - -/** - * Load a [FontFamily] embedded in the JetBrains Runtime. It will return - * `null` if the font family isn't embedded in the JetBrains Runtime, - * or if using any Java runtime other than the JetBrains Runtime. - * - * Using this requires the current module to have access to the `sun.font` - * APIs, which are closed by default. To open the access to those APIs, you - * need to pass this argument to the JVM: - * ``` - * --add-opens java.desktop/sun.font=ALL-UNNAMED - * ``` - * - * If the `sun.font` API is not accessible, this will always return `null`. - * - * @param familyName The case-insensitive font family name to load. - * @return the requested font family, if running on the JetBrains Runtime, - * and the `sun.font` API is accessible, and the font family exists in - * the runtime. Otherwise, `null`. - */ -@ExperimentalTextApi -@Stable -fun EmbeddedFontFamily(familyName: String): FontFamily? = - JetBrainsRuntimeFontFamilies.embeddedFamilies[familyName.lowercase()] diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.desktop.kt similarity index 100% rename from compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.desktop.kt rename to compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.desktop.kt diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/intl/DesktopPlatformLocale.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/intl/DesktopPlatformLocale.desktop.kt index a25080f758dab..82f1244e23a7a 100644 --- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/intl/DesktopPlatformLocale.desktop.kt +++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/intl/DesktopPlatformLocale.desktop.kt @@ -16,6 +16,7 @@ package androidx.compose.ui.text.intl +import androidx.compose.ui.InternalComposeUiApi import java.awt.ComponentOrientation import java.util.Locale as JavaLocale @@ -24,6 +25,7 @@ internal actual fun createPlatformLocaleDelegate() = object : PlatformLocaleDele get() = LocaleList(listOf(Locale(JavaLocale.getDefault()))) } -internal actual fun Locale.isRtl(): Boolean = +@InternalComposeUiApi +actual fun Locale.isRtl(): Boolean = // TODO Get rid of AWT reference here !ComponentOrientation.getOrientation(this.platformLocale).isLeftToRight diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.desktop.kt index bb748200a0782..9da95e606cd33 100644 --- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.desktop.kt +++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.desktop.kt @@ -15,6 +15,7 @@ */ package androidx.compose.ui.text.platform +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -23,17 +24,12 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontVariation import androidx.compose.ui.text.font.FontWeight import java.io.File -import org.jetbrains.skia.Data -import org.jetbrains.skia.FontMgr -import org.jetbrains.skia.FontSlant -import org.jetbrains.skia.FontStyle as SkFontStyle -import org.jetbrains.skia.FontWidth -import org.jetbrains.skia.Typeface as SkTypeface actual sealed class PlatformFont : Font { actual abstract val identity: String actual abstract val variationSettings: FontVariation.Settings - internal actual val cacheKey: String + @InternalComposeUiApi + actual val cacheKey: String get() = "${this::class.qualifiedName}|$identity|weight=${weight.weight}|style=$style" } @@ -50,13 +46,14 @@ actual sealed class PlatformFont : Font { * @see FontFamily */ -class ResourceFont internal constructor( +class ResourceFont @InternalComposeUiApi constructor( val name: String, override val weight: FontWeight = FontWeight.Normal, override val style: FontStyle = FontStyle.Normal, override val variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style), ) : PlatformFont() { + @OptIn(InternalComposeUiApi::class) constructor( name: String, weight: FontWeight = FontWeight.Normal, @@ -94,31 +91,6 @@ class ResourceFont internal constructor( } } -/** - * Creates a Font using a resource name. - * - * @param resource The resource name in classpath. - * @param weight The weight of the font. The system uses this to match a - * font to a font request that is given in a - * [androidx.compose.ui.text.SpanStyle]. - * @param style The style of the font, normal or italic. The system uses - * this to match a font to a font request that is given in a - * [androidx.compose.ui.text.SpanStyle]. - * @see FontFamily - */ -fun Font( - resource: String, - weight: FontWeight = FontWeight.Normal, - style: FontStyle = FontStyle.Normal -): Font = ResourceFont(resource, weight, style, FontVariation.Settings()) - -fun Font( - resource: String, - weight: FontWeight = FontWeight.Normal, - style: FontStyle = FontStyle.Normal, - variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style) -): Font = ResourceFont(resource, weight, style, variationSettings) - /** * Defines a Font using a file path. * @@ -131,13 +103,14 @@ fun Font( * [androidx.compose.ui.text.SpanStyle]. * @see FontFamily */ -class FileFont internal constructor( +class FileFont @InternalComposeUiApi constructor( val file: File, override val weight: FontWeight = FontWeight.Normal, override val style: FontStyle = FontStyle.Normal, override val variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style), ) : PlatformFont() { + @OptIn(InternalComposeUiApi::class) constructor( file: File, weight: FontWeight = FontWeight.Normal, @@ -174,69 +147,3 @@ class FileFont internal constructor( return "FileFont(file=$file, weight=$weight, style=$style, variationSettings=${variationSettings.settings})" } } - -/** - * Creates a Font using a file path. - * - * @param file File path to font. - * @param weight The weight of the font. The system uses this to match a - * font to a font request that is given in a - * [androidx.compose.ui.text.SpanStyle]. - * @param style The style of the font, normal or italic. The system uses - * this to match a font to a font request that is given in a - * [androidx.compose.ui.text.SpanStyle]. - * @see FontFamily - */ -fun Font( - file: File, - weight: FontWeight = FontWeight.Normal, - style: FontStyle = FontStyle.Normal -): Font = FileFont(file, weight, style, FontVariation.Settings()) - -fun Font( - file: File, - weight: FontWeight = FontWeight.Normal, - style: FontStyle = FontStyle.Normal, - variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style) -): Font = FileFont(file, weight, style, variationSettings) - -internal actual fun loadTypeface(font: Font): SkTypeface { - if (font !is PlatformFont) { - throw IllegalArgumentException("Unsupported font type: $font") - } - val typeface = when (font) { - is ResourceFont -> typefaceResource(font.name) - is FileFont -> FontMgr.default.makeFromFile(font.file.toString()) - is LoadedFont -> FontMgr.default.makeFromData(Data.makeFromBytes(font.getData())) - is SystemFont -> FontMgr.default.matchFamilyStyle(font.identity, font.skFontStyle) - } ?: (FontMgr.default.legacyMakeTypeface(font.identity, font.skFontStyle) - ?: error("loadTypeface legacyMakeTypeface failed")) - return typeface.cloneWithVariationSettings(font.variationSettings) -} - -private fun typefaceResource(resourceName: String): SkTypeface { - val contextClassLoader = Thread.currentThread().contextClassLoader!! - val resource = contextClassLoader.getResourceAsStream(resourceName) - ?: (::typefaceResource.javaClass).getResourceAsStream(resourceName) - ?: error("Can't load font from $resourceName") - - val bytes = resource.use { it.readAllBytes() } - return FontMgr.default.makeFromData(Data.makeFromBytes(bytes))!! -} - -private val Font.skFontStyle: SkFontStyle - get() = SkFontStyle( - weight = weight.weight, - width = FontWidth.NORMAL, - slant = if (style == FontStyle.Italic) FontSlant.ITALIC else FontSlant.UPRIGHT - ) - -internal actual fun currentPlatform(): Platform { - val name = System.getProperty("os.name") - return when { - name.startsWith("Linux") -> Platform.Linux - name.startsWith("Win") -> Platform.Windows - name == "Mac OS X" -> Platform.MacOS - else -> Platform.Unknown - } -} diff --git a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphIntegrationTest.kt b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphIntegrationTest.kt index 476cf607be1d0..4ecc4c2c4a53a 100644 --- a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphIntegrationTest.kt +++ b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphIntegrationTest.kt @@ -15,10 +15,12 @@ */ package androidx.compose.ui.text +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathOperation +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight @@ -39,6 +41,7 @@ import androidx.kruth.FloatSubject import androidx.kruth.Subject import androidx.kruth.assertThat import androidx.kruth.assertWithMessage +import kotlin.test.BeforeTest import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals @@ -47,8 +50,23 @@ import kotlin.test.assertFailsWith // TODO: move to commonTest once sample_font will be available outside of JVM // Adopted tests from androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt +@OptIn(InternalComposeUiApi::class) class DesktopParagraphIntegrationTest { - private val fontFamilyResolver = createFontFamilyResolver() + @BeforeTest + fun setup() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once tests that register the backend asynchronously + // (e.g. AWT ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on + // the registration persisting across tests. + // @AfterTest + // fun clearSkikoBackend() { + // clearSkikoComposeImplementation() + // } + + // Lazy so the registry is populated by [setup] before the resolver is created. + private val fontFamilyResolver by lazy { createFontFamilyResolver() } private val fontFamilyMeasureFont = FontFamily( Font( diff --git a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt index 2c9c9c7d0ecb2..f1ffe00661fda 100644 --- a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt +++ b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt @@ -16,9 +16,11 @@ package androidx.compose.ui.text +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -38,17 +40,32 @@ import androidx.compose.ui.unit.sp import com.google.common.truth.FloatSubject import com.google.common.truth.Truth.assertThat import kotlin.math.roundToInt +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @RunWith(JUnit4::class) +@OptIn(InternalComposeUiApi::class) class DesktopParagraphTest { @get:Rule val rule = createComposeRule() - private val fontFamilyResolver = createFontFamilyResolver() + @Before + fun setup() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once async-registration tests (AWT) no longer rely on the + // backend registration persisting across tests. + // @After + // fun cleanup() { + // clearSkikoComposeImplementation() + // } + + // Lazy so the registry is populated by [setup] before the resolver is created. + private val fontFamilyResolver by lazy { createFontFamilyResolver() } private val defaultDensity = Density(density = 1f) private val fontFamilyMeasureFont = FontFamily( diff --git a/compose/ui/ui-text/src/macosMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.macos.kt b/compose/ui/ui-text/src/macosMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.macos.kt similarity index 100% rename from compose/ui/ui-text/src/macosMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.macos.kt rename to compose/ui/ui-text/src/macosMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.macos.kt diff --git a/compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.native.kt b/compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.native.kt index 525b74d8933ce..19993f1811e00 100644 --- a/compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.native.kt +++ b/compose/ui/ui-text/src/nativeMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.native.kt @@ -15,12 +15,14 @@ */ package androidx.compose.ui.text.platform +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontVariation actual sealed class PlatformFont : Font { actual abstract val identity: String actual abstract val variationSettings: FontVariation.Settings - internal actual val cacheKey: String + @InternalComposeUiApi + actual val cacheKey: String get() = "${this::class.qualifiedName}|$identity|weight=${weight.weight}|style=$style|variationSettings=${variationSettings.settings}" } diff --git a/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/CharHelpers.nonAndroid.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/CharHelpers.nonAndroid.kt new file mode 100644 index 0000000000000..f677f886415de --- /dev/null +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/CharHelpers.nonAndroid.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.text + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.text.platform.PlatformTextRegistry + +@OptIn(InternalComposeUiApi::class) +internal actual fun String.findPrecedingBreak(index: Int): Int { + return PlatformTextRegistry.requireCurrent().findPrecedingBreak(this, index) +} + +@OptIn(InternalComposeUiApi::class) +internal actual fun String.findFollowingBreak(index: Int): Int { + return PlatformTextRegistry.requireCurrent().findFollowingBreak(this, index) +} diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/FontRasterizationSettings.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/FontRasterizationSettings.nonAndroid.kt similarity index 50% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/FontRasterizationSettings.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/FontRasterizationSettings.nonAndroid.kt index 583ec17182140..175203c568f24 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/FontRasterizationSettings.skiko.kt +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/FontRasterizationSettings.nonAndroid.kt @@ -16,8 +16,8 @@ package androidx.compose.ui.text -import androidx.compose.ui.text.platform.Platform -import androidx.compose.ui.text.platform.currentPlatform +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.text.platform.PlatformTextRegistry /** * Whether edge pixels draw opaque or with partial transparency. @@ -74,40 +74,14 @@ class FontRasterizationSettings( val autoHintingForced: Boolean ) { companion object { - val PlatformDefault by lazy { - when (currentPlatform()) { - Platform.Windows -> FontRasterizationSettings( - subpixelPositioning = true, - // Most UIs still use ClearType on Windows, so we should match this - // We temporarily disabled `SubpixelAntiAlias` until we figure out - // how to properly retrieve default OS settings - smoothing = FontSmoothing.AntiAlias, - hinting = FontHinting.Normal, // None would trigger some potentially unwanted behavior, but everything else is forced into Normal on Windows - autoHintingForced = false, - ) - - Platform.Linux, Platform.Unknown -> FontRasterizationSettings( - subpixelPositioning = true, - smoothing = FontSmoothing.AntiAlias, - hinting = FontHinting.Slight, // Most distributions use Slight now by default - autoHintingForced = false, - ) - - Platform.Android -> FontRasterizationSettings( - subpixelPositioning = true, - smoothing = FontSmoothing.AntiAlias, - hinting = FontHinting.Slight, - autoHintingForced = false, - ) - - Platform.MacOS, Platform.IOS, Platform.TvOS, Platform.WatchOS -> FontRasterizationSettings( - subpixelPositioning = true, - smoothing = FontSmoothing.AntiAlias, // macOS doesn't support SubpixelAntiAlias anymore as of Catalina - hinting = FontHinting.Normal, // Completely ignored on macOS - autoHintingForced = false, // Completely ignored on macOS - ) - } - } + /** + * The platform-default rasterization settings. The concrete values are produced by the + * registered text backend (see [androidx.compose.ui.text.platform.PlatformText.defaultFontRasterizationSettings]), keeping + * the platform/skia-specific detection out of ui-text. + */ + @OptIn(InternalComposeUiApi::class) + val PlatformDefault: FontRasterizationSettings + get() = PlatformTextRegistry.requireCurrent().defaultFontRasterizationSettings } override fun equals(other: Any?): Boolean { @@ -137,18 +111,3 @@ class FontRasterizationSettings( "autoHintingForced=$autoHintingForced)" } } - -@OptIn(ExperimentalTextApi::class) -internal fun FontSmoothing.toSkFontEdging() = when (this) { - FontSmoothing.None -> org.jetbrains.skia.FontEdging.ALIAS - FontSmoothing.AntiAlias -> org.jetbrains.skia.FontEdging.ANTI_ALIAS - FontSmoothing.SubpixelAntiAlias -> org.jetbrains.skia.FontEdging.SUBPIXEL_ANTI_ALIAS -} - -@OptIn(ExperimentalTextApi::class) -internal fun FontHinting.toSkFontHinting() = when (this) { - FontHinting.None -> org.jetbrains.skia.FontHinting.NONE - FontHinting.Slight -> org.jetbrains.skia.FontHinting.SLIGHT - FontHinting.Normal -> org.jetbrains.skia.FontHinting.NORMAL - FontHinting.Full -> org.jetbrains.skia.FontHinting.FULL -} diff --git a/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/Paragraph.nonAndroid.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/Paragraph.nonAndroid.kt new file mode 100644 index 0000000000000..77871b3a96391 --- /dev/null +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/Paragraph.nonAndroid.kt @@ -0,0 +1,268 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmMultifileClass +@file:JvmName("ParagraphKt") +@file:OptIn(InternalComposeUiApi::class) + +package androidx.compose.ui.text + +import androidx.annotation.IntRange +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.graphics.drawscope.DrawStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.createFontFamilyResolver +import androidx.compose.ui.text.internal.JvmDefaultWithCompatibility +import androidx.compose.ui.text.platform.PlatformTextRegistry +import androidx.compose.ui.text.style.ResolvedTextDirection +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +@JvmDefaultWithCompatibility +actual sealed interface Paragraph { + actual val width: Float + actual val height: Float + actual val minIntrinsicWidth: Float + actual val maxIntrinsicWidth: Float + actual val firstBaseline: Float + actual val lastBaseline: Float + actual val didExceedMaxLines: Boolean + actual val lineCount: Int + actual val placeholderRects: List + actual fun getPathForRange(start: Int, end: Int): Path + actual fun getCursorRect(offset: Int): Rect + actual fun getLineLeft(lineIndex: Int): Float + actual fun getLineRight(lineIndex: Int): Float + actual fun getLineTop(lineIndex: Int): Float + actual fun getLineBaseline(lineIndex: Int): Float + actual fun getLineBottom(lineIndex: Int): Float + actual fun getLineHeight(lineIndex: Int): Float + actual fun getLineWidth(lineIndex: Int): Float + actual fun getLineStart(lineIndex: Int): Int + actual fun getLineEnd(lineIndex: Int, visibleEnd: Boolean): Int + actual fun isLineEllipsized(lineIndex: Int): Boolean + actual fun getLineForOffset(offset: Int): Int + actual fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float + actual fun getParagraphDirection(offset: Int): ResolvedTextDirection + actual fun getBidiRunDirection(offset: Int): ResolvedTextDirection + actual fun getLineForVerticalPosition(vertical: Float): Int + actual fun getOffsetForPosition(position: Offset): Int + actual fun getRangeForRect( + rect: Rect, + granularity: TextGranularity, + inclusionStrategy: TextInclusionStrategy + ): TextRange + actual fun getBoundingBox(offset: Int): Rect + actual fun fillBoundingBoxes(range: TextRange, array: FloatArray, @IntRange(from = 0) arrayStart: Int) + actual fun getWordBoundary(offset: Int): TextRange + actual fun paint(canvas: Canvas, color: Color, shadow: Shadow?, textDecoration: TextDecoration?) + actual fun paint( + canvas: Canvas, + color: Color, + shadow: Shadow?, + textDecoration: TextDecoration?, + drawStyle: DrawStyle?, + blendMode: BlendMode + ) + actual fun paint( + canvas: Canvas, + brush: Brush, + alpha: Float, + shadow: Shadow?, + textDecoration: TextDecoration?, + drawStyle: DrawStyle?, + blendMode: BlendMode + ) +} + +@Suppress("DEPRECATION") +@Deprecated( + "Font.ResourceLoader is deprecated, instead pass FontFamily.Resolver", + replaceWith = + ReplaceWith( + "ActualParagraph(text, style, spanStyles, placeholders, " + + "maxLines, ellipsis, width, density, createFontFamilyResolver(resourceLoader))" + ), +) +actual fun Paragraph( + text: String, + style: TextStyle, + spanStyles: List>, + placeholders: List>, + maxLines: Int, + ellipsis: Boolean, + width: Float, + density: Density, + resourceLoader: Font.ResourceLoader, +): Paragraph = + PlatformTextRegistry.requireCurrent().createParagraph( + text = text, + style = style, + annotations = spanStyles, + placeholders = placeholders, + maxLines = maxLines, + overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip, + constraints = Constraints(maxWidth = width.ceilToInt()), + density = density, + fontFamilyResolver = createFontFamilyResolver(resourceLoader), + ) + +@Deprecated( + "Paragraph that takes maximum allowed width is deprecated, pass constraints instead.", + ReplaceWith( + "Paragraph(text, style, Constraints(maxWidth = ceil(width).toInt()), density, " + + "fontFamilyResolver, spanStyles, placeholders, maxLines, ellipsis)", + "kotlin.math.ceil", + "androidx.compose.ui.unit.Constraints", + ), +) +actual fun Paragraph( + text: String, + style: TextStyle, + width: Float, + density: Density, + fontFamilyResolver: FontFamily.Resolver, + spanStyles: List>, + placeholders: List>, + maxLines: Int, + ellipsis: Boolean, +): Paragraph = + PlatformTextRegistry.requireCurrent().createParagraph( + text = text, + style = style, + annotations = spanStyles, + placeholders = placeholders, + maxLines = maxLines, + overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip, + constraints = Constraints(maxWidth = width.ceilToInt()), + density = density, + fontFamilyResolver = fontFamilyResolver, + ) + +@Deprecated( + "Paragraph that takes `ellipsis: Boolean` is deprecated, pass TextOverflow instead.", + level = DeprecationLevel.HIDDEN, +) +actual fun Paragraph( + text: String, + style: TextStyle, + constraints: Constraints, + density: Density, + fontFamilyResolver: FontFamily.Resolver, + spanStyles: List>, + placeholders: List>, + maxLines: Int, + ellipsis: Boolean, +): Paragraph = + PlatformTextRegistry.requireCurrent().createParagraph( + text = text, + style = style, + annotations = spanStyles, + placeholders = placeholders, + maxLines = maxLines, + overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip, + constraints = constraints, + density = density, + fontFamilyResolver = fontFamilyResolver, + ) + +actual fun Paragraph( + text: String, + style: TextStyle, + constraints: Constraints, + density: Density, + fontFamilyResolver: FontFamily.Resolver, + spanStyles: List>, + placeholders: List>, + maxLines: Int, + overflow: TextOverflow, +): Paragraph = + PlatformTextRegistry.requireCurrent().createParagraph( + text = text, + style = style, + annotations = spanStyles, + placeholders = placeholders, + maxLines = maxLines, + overflow = overflow, + constraints = constraints, + density = density, + fontFamilyResolver = fontFamilyResolver, + ) + +@Deprecated( + "Paragraph that takes maximum allowed width is deprecated, pass constraints instead.", + ReplaceWith( + "Paragraph(paragraphIntrinsics, Constraints(maxWidth = ceil(width).toInt()), maxLines, " + + "ellipsis)", + "kotlin.math.ceil", + "androidx.compose.ui.unit.Constraints", + ), +) +actual fun Paragraph( + paragraphIntrinsics: ParagraphIntrinsics, + maxLines: Int, + ellipsis: Boolean, + width: Float, +): Paragraph = + PlatformTextRegistry.requireCurrent().createParagraph( + paragraphIntrinsics = paragraphIntrinsics, + maxLines = maxLines, + overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip, + constraints = Constraints(maxWidth = width.ceilToInt()), + ) + +@Deprecated( + "Paragraph that takes ellipsis: Boolean is deprecated, pass TextOverflow instead.", + level = DeprecationLevel.HIDDEN, +) +actual fun Paragraph( + paragraphIntrinsics: ParagraphIntrinsics, + constraints: Constraints, + maxLines: Int, + ellipsis: Boolean, +): Paragraph = + PlatformTextRegistry.requireCurrent().createParagraph( + paragraphIntrinsics = paragraphIntrinsics, + maxLines = maxLines, + overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip, + constraints = constraints, + ) + +actual fun Paragraph( + paragraphIntrinsics: ParagraphIntrinsics, + constraints: Constraints, + maxLines: Int, + overflow: TextOverflow, +): Paragraph = + PlatformTextRegistry.requireCurrent().createParagraph( + paragraphIntrinsics = paragraphIntrinsics, + maxLines = maxLines, + overflow = overflow, + constraints = constraints, + ) diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/ParagraphIntrisics.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/ParagraphIntrisics.nonAndroid.kt similarity index 88% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/ParagraphIntrisics.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/ParagraphIntrisics.nonAndroid.kt index 085f079ccade7..ee77672069184 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/ParagraphIntrisics.skiko.kt +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/ParagraphIntrisics.nonAndroid.kt @@ -15,12 +15,14 @@ */ @file:JvmName("ParagraphIntrinsicsKt") +@file:OptIn(InternalComposeUiApi::class) package androidx.compose.ui.text -import androidx.compose.ui.text.platform.SkiaParagraphIntrinsics +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.createFontFamilyResolver +import androidx.compose.ui.text.platform.PlatformTextRegistry import androidx.compose.ui.unit.Density import kotlin.jvm.JvmName @@ -39,7 +41,7 @@ actual fun ParagraphIntrinsics( density: Density, resourceLoader: Font.ResourceLoader, ): ParagraphIntrinsics = - SkiaParagraphIntrinsics( + PlatformTextRegistry.requireCurrent().createParagraphIntrinsics( text = text, style = style, annotations = spanStyles, @@ -62,7 +64,7 @@ actual fun ParagraphIntrinsics( density: Density, fontFamilyResolver: FontFamily.Resolver, ): ParagraphIntrinsics = - SkiaParagraphIntrinsics( + PlatformTextRegistry.requireCurrent().createParagraphIntrinsics( text = text, style = style, annotations = spanStyles, @@ -79,7 +81,7 @@ actual fun ParagraphIntrinsics( fontFamilyResolver: FontFamily.Resolver, placeholders: List>, ): ParagraphIntrinsics = - SkiaParagraphIntrinsics( + PlatformTextRegistry.requireCurrent().createParagraphIntrinsics( text = text, style = style, annotations = annotations, @@ -97,7 +99,7 @@ actual fun ParagraphIntrinsics( placeholders: List>, softWrap: Boolean, ): ParagraphIntrinsics = - SkiaParagraphIntrinsics( + PlatformTextRegistry.requireCurrent().createParagraphIntrinsics( text = text, style = style, annotations = annotations, @@ -105,3 +107,4 @@ actual fun ParagraphIntrinsics( density = density, fontFamilyResolver = fontFamilyResolver, ) + diff --git a/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/PlatformParagraph.nonAndroid.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/PlatformParagraph.nonAndroid.kt new file mode 100644 index 0000000000000..d77faf3652e28 --- /dev/null +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/PlatformParagraph.nonAndroid.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Must stay in package androidx.compose.ui.text: Paragraph is a sealed interface, so its subtypes +// have to be declared in the same package. +package androidx.compose.ui.text + +import androidx.compose.ui.InternalComposeUiApi + +/** + * Platform [Paragraph] supplied by the registered ui-text backend. The backend implements it + * directly (so no wrapper is needed) and may add line-metric accessors that are not part of the + * common [Paragraph] API. + */ +@InternalComposeUiApi +interface PlatformParagraph : Paragraph { + /** Returns the ascent (distance above the baseline, in pixels) of the line at [lineIndex]. */ + fun getLineAscent(lineIndex: Int): Float + + /** Returns the descent (distance below the baseline, in pixels) of the line at [lineIndex]. */ + fun getLineDescent(lineIndex: Int): Float +} diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Savers.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/Savers.nonAndroid.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Savers.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/Savers.nonAndroid.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/TextDecorationLineStyle.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/TextDecorationLineStyle.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/TextDecorationLineStyle.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/TextDecorationLineStyle.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/TextStyle.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/TextStyle.nonAndroid.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/TextStyle.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/TextStyle.nonAndroid.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/UnresolvedSymbolsRegistry.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/UnresolvedSymbolsRegistry.nonAndroid.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/UnresolvedSymbolsRegistry.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/UnresolvedSymbolsRegistry.nonAndroid.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/WeakReference.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/WeakReference.nonAndroid.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/WeakReference.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/WeakReference.nonAndroid.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.nonAndroid.kt similarity index 69% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.nonAndroid.kt index 00196604a8d1d..888f0d2059eec 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.kt +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.nonAndroid.kt @@ -1,5 +1,3 @@ -@file:JvmName("FontFamilyResolver_sikioKt") - /* * Copyright 2022 The Android Open Source Project * @@ -16,10 +14,13 @@ * limitations under the License. */ +@file:JvmName("FontFamilyResolver_sikioKt") + package androidx.compose.ui.text.font +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.text.ExperimentalTextApi -import androidx.compose.ui.text.platform.FontCache +import androidx.compose.ui.text.platform.PlatformTextRegistry import kotlin.coroutines.CoroutineContext import kotlin.jvm.JvmName @@ -32,11 +33,9 @@ import kotlin.jvm.JvmName * * Usages inside of Composition should use LocalFontFamilyResolver.current */ +@OptIn(InternalComposeUiApi::class) fun createFontFamilyResolver(): FontFamily.Resolver { - return FontFamilyResolverImpl( - SkiaFontLoader(), - createPlatformResolveInterceptor() - ) + return PlatformTextRegistry.requireCurrent().createFontFamilyResolver() } /** @@ -59,11 +58,31 @@ fun createFontFamilyResolver(): FontFamily.Resolver { * @param coroutineContext context to launch async requests in during resolution. */ @ExperimentalTextApi -fun createFontFamilyResolver( - coroutineContext: CoroutineContext +@OptIn(InternalComposeUiApi::class) +fun createFontFamilyResolver(coroutineContext: CoroutineContext): FontFamily.Resolver { + return PlatformTextRegistry.requireCurrent().createFontFamilyResolver(coroutineContext) +} + +/** + * Builds the internal [FontFamilyResolverImpl] from the [backend] supplied by the registered + * backend. All resolver internals stay in ui-text; the backend only provides the loader seam. + */ +@InternalComposeUiApi +fun createPlatformFontFamilyResolver(backend: PlatformTypefacesLoader): FontFamily.Resolver { + return FontFamilyResolverImpl( + PlatformFontLoaderAdapter(backend), + createPlatformResolveInterceptor(), + ) +} + +@OptIn(InternalComposeUiApi::class, ExperimentalTextApi::class) +@InternalComposeUiApi +fun createPlatformFontFamilyResolver( + backend: PlatformTypefacesLoader, + coroutineContext: CoroutineContext, ): FontFamily.Resolver { return FontFamilyResolverImpl( - SkiaFontLoader(), + PlatformFontLoaderAdapter(backend), createPlatformResolveInterceptor(), GlobalTypefaceRequestCache, FontListFontFamilyTypefaceAdapter( @@ -72,11 +91,5 @@ fun createFontFamilyResolver( ) ) } -/** - * For bridging between FontLoader and FontFamily.ResourceLoader. Can remove with FontLoader. - */ -internal fun createFontFamilyResolver(fontCache: FontCache): FontFamily.Resolver { - return FontFamilyResolverImpl(SkiaFontLoader(fontCache), createPlatformResolveInterceptor()) -} internal expect fun createPlatformResolveInterceptor(): PlatformResolveInterceptor diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsage.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/FontResourceLoaderWithResolver.nonAndroid.kt similarity index 68% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsage.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/FontResourceLoaderWithResolver.nonAndroid.kt index 8059b0c9e506f..c134413b1ce9c 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/DelegatingFontLoaderForDeprecatedUsage.skiko.kt +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/FontResourceLoaderWithResolver.nonAndroid.kt @@ -13,12 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:Suppress("DEPRECATION") package androidx.compose.ui.text.font -import androidx.compose.ui.text.platform.FontLoader +import androidx.compose.ui.InternalComposeUiApi +/** + * Seam implemented by the backend's deprecated `FontLoader` (a [Font.ResourceLoader]) so this + * bridge can hand back its [FontFamily.Resolver] without referencing the backend-specific loader + * type. + */ +@InternalComposeUiApi +interface FontResourceLoaderWithResolver { + val fontFamilyResolver: FontFamily.Resolver +} + +@Suppress("DEPRECATION") +@OptIn(InternalComposeUiApi::class) @Deprecated("This exists to bridge existing Font.ResourceLoader APIs, and should be " + "removed with them", replaceWith = ReplaceWith("createFontFamilyResolver()"), @@ -26,7 +37,7 @@ import androidx.compose.ui.text.platform.FontLoader internal actual fun createFontFamilyResolver( fontResourceLoader: Font.ResourceLoader ): FontFamily.Resolver { - if (fontResourceLoader !is FontLoader) + if (fontResourceLoader !is FontResourceLoaderWithResolver) throw IllegalArgumentException("Unexpected type: $fontResourceLoader must be FontLoader") return fontResourceLoader.fontFamilyResolver } diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapter.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapter.nonAndroid.kt similarity index 85% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapter.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapter.nonAndroid.kt index b29a3605d2f10..d24f3a0d0b746 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapter.skiko.kt +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/PlatformFontFamilyTypefaceAdapter.nonAndroid.kt @@ -16,6 +16,9 @@ package androidx.compose.ui.text.font +import androidx.compose.ui.InternalComposeUiApi + +@OptIn(InternalComposeUiApi::class) internal actual class PlatformFontFamilyTypefaceAdapter actual constructor() : FontFamilyTypefaceAdapter { @@ -26,12 +29,12 @@ internal actual class PlatformFontFamilyTypefaceAdapter actual constructor() : createDefaultTypeface: (TypefaceRequest) -> Any ): TypefaceResult? { if (typefaceRequest.fontFamily is FontListFontFamily) return null - val skiaFontLoader = (platformFontLoader as SkiaFontLoader) - val result = skiaFontLoader.loadPlatformTypes( + val backend = (platformFontLoader as PlatformFontLoaderAdapter).backend + val result = backend.loadPlatformTypes( typefaceRequest.fontFamily ?: FontFamily.Default, typefaceRequest.fontWeight, typefaceRequest.fontStyle ) return TypefaceResult.Immutable(result) } -} \ No newline at end of file +} diff --git a/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/PlatformTypefacesLoader.nonAndroid.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/PlatformTypefacesLoader.nonAndroid.kt new file mode 100644 index 0000000000000..11a2d16d04869 --- /dev/null +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/font/PlatformTypefacesLoader.nonAndroid.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.text.font + +import androidx.compose.ui.InternalComposeUiApi + +/** + * Font backend supplied by the registered backend. ui-text keeps the resolver wiring + * (`FontFamilyResolverImpl`, the typeface adapters, the request caches — all internal to ui-text) + * backend-agnostic, and adapts this seam into the internal [PlatformFontLoader] via + * [createPlatformFontFamilyResolver]. Values typed as [Any] are opaque backend handles that only the + * backend interprets (the platform font-load result and the platform font collection). + */ +@InternalComposeUiApi +interface PlatformTypefacesLoader { + /** @see PlatformFontLoader.loadBlocking */ + fun loadBlocking(font: Font): Any? + + /** @see PlatformFontLoader.awaitLoad */ + suspend fun awaitLoad(font: Font): Any? + + /** @see PlatformFontLoader.cacheKey */ + val cacheKey: Any? + + /** Resolves a system/loaded typeface for [fontFamily]; result is wrapped into a TypefaceResult. */ + fun loadPlatformTypes( + fontFamily: FontFamily, + fontWeight: FontWeight, + fontStyle: FontStyle, + ): Any + + /** The platform font collection used for paragraph building. */ + val fontCollection: Any +} + +/** Adapts the public [PlatformTypefacesLoader] seam to the internal [PlatformFontLoader]. */ +@OptIn(InternalComposeUiApi::class) +internal class PlatformFontLoaderAdapter( + val backend: PlatformTypefacesLoader, +) : PlatformFontLoader { + override fun loadBlocking(font: Font): Any? = backend.loadBlocking(font) + + override suspend fun awaitLoad(font: Font): Any? = backend.awaitLoad(font) + + override val cacheKey: Any? + get() = backend.cacheKey +} + +/** + * Returns the [PlatformTypefacesLoader] backing [this] resolver, or `null` if it was not built by + * [createPlatformFontFamilyResolver]. Used by the backend to reach the platform font collection. + */ +@InternalComposeUiApi +fun FontFamily.Resolver.platformTypefacesLoader(): PlatformTypefacesLoader? { + val loader = (this as? FontFamilyResolverImpl)?.platformFontLoader + return (loader as? PlatformFontLoaderAdapter)?.backend +} diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.nonAndroid.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.nonAndroid.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputService2.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputService2.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputService2.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputService2.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.nonAndroid.kt similarity index 86% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.nonAndroid.kt index f2e5d2464297f..31ca71afc6cb7 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.skiko.kt +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.nonAndroid.kt @@ -16,4 +16,7 @@ package androidx.compose.ui.text.intl -internal expect fun Locale.isRtl(): Boolean +import androidx.compose.ui.InternalComposeUiApi + +@InternalComposeUiApi +expect fun Locale.isRtl(): Boolean diff --git a/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.nonAndroid.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.nonAndroid.kt new file mode 100644 index 0000000000000..8d03df976d208 --- /dev/null +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.nonAndroid.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.text.platform + +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontLoadingStrategy +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontVariation +import androidx.compose.ui.text.font.FontWeight + +expect sealed class PlatformFont() : Font { + abstract val identity: String + abstract val variationSettings: FontVariation.Settings + + /** Used by the registered font backend to key typefaces. */ + @InternalComposeUiApi + val cacheKey: String +} + +/** + * A Font that's already installed in the system. + * + * @param identity Unique identity for a font. Used internally to distinguish fonts. + * @param weight The weight of the font. The system uses this to match a font to a font request + * that is given in a [androidx.compose.ui.text.SpanStyle]. + * @param style The style of the font, normal or italic. The system uses this to match a font to a + * font request that is given in a [androidx.compose.ui.text.SpanStyle]. + * + * @see FontFamily + */ +@ExperimentalTextApi +class SystemFont( + override val identity: String, + override val weight: FontWeight = FontWeight.Normal, + override val style: FontStyle = FontStyle.Normal, + override val variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style), +) : PlatformFont() { + + constructor( + identity: String, + weight: FontWeight = FontWeight.Normal, + style: FontStyle = FontStyle.Normal + ) : this(identity, weight, style, variationSettings = FontVariation.Settings()) + + override fun toString(): String { + return "SystemFont(identity='$identity', weight=$weight, style=$style, variationSettings=${variationSettings.settings})" + } +} + +/** + * Defines a Font using a byte array with loaded font data. + * + * @param identity Unique identity for a font. Used internally to distinguish fonts. + * @param getData should return Byte array with loaded font data. + * @param weight The weight of the font. The system uses this to match a font to a font request + * that is given in a [androidx.compose.ui.text.SpanStyle]. + * @param style The style of the font, normal or italic. The system uses this to match a font to a + * font request that is given in a [androidx.compose.ui.text.SpanStyle]. + * + * @see FontFamily + */ +class LoadedFont @InternalComposeUiApi constructor( + override val identity: String, + internal val getData: () -> ByteArray, + override val weight: FontWeight, + override val style: FontStyle, + override val variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style), +) : PlatformFont() { + + @OptIn(InternalComposeUiApi::class) + constructor( + identity: String, + getData: () -> ByteArray, + weight: FontWeight, + style: FontStyle + ) : this(identity, getData, weight, style, FontVariation.Settings()) + + @ExperimentalTextApi + override val loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Blocking + + val data: ByteArray get() = getData() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is LoadedFont) return false + if (identity != other.identity) return false + if (weight != other.weight) return false + if (style != other.style) return false + return variationSettings.settings == other.variationSettings.settings + } + + override fun hashCode(): Int { + var result = identity.hashCode() + result = 31 * result + weight.hashCode() + result = 31 * result + style.hashCode() + result = 31 * result + variationSettings.settings.hashCode() + return result + } + + override fun toString(): String { + return "LoadedFont(identity='$identity', weight=$weight, style=$style, variationSettings=${variationSettings.settings})" + } +} diff --git a/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/PlatformText.nonAndroid.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/PlatformText.nonAndroid.kt new file mode 100644 index 0000000000000..b154f5e7ce059 --- /dev/null +++ b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/PlatformText.nonAndroid.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.text.platform + +import androidx.annotation.VisibleForTesting +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.FontRasterizationSettings +import androidx.compose.ui.text.ParagraphIntrinsics +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlatformParagraph +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import kotlin.coroutines.CoroutineContext +import kotlinx.atomicfu.locks.SynchronizedObject +import kotlinx.atomicfu.locks.synchronized + +/** + * Factory surface a text backend implements to provide ui-text's platform primitives. + * + * The implementation is registered at runtime, so text users compile against stable APIs without + * depending on a specific backend. + */ +@InternalComposeUiApi +interface PlatformText { + /** Lays out [text] with [style] into a backend [PlatformParagraph] bounded by [constraints]. */ + fun createParagraph( + text: String, + style: TextStyle, + annotations: List>, + placeholders: List>, + maxLines: Int, + overflow: TextOverflow, + constraints: Constraints, + density: Density, + fontFamilyResolver: FontFamily.Resolver, + ): PlatformParagraph + + /** Re-lays out a previously measured [paragraphIntrinsics] under new [constraints]. */ + fun createParagraph( + paragraphIntrinsics: ParagraphIntrinsics, + maxLines: Int, + overflow: TextOverflow, + constraints: Constraints, + ): PlatformParagraph + + /** Measures [text] with [style] into reusable [ParagraphIntrinsics]. */ + fun createParagraphIntrinsics( + text: String, + style: TextStyle, + annotations: List>, + placeholders: List>, + density: Density, + fontFamilyResolver: FontFamily.Resolver, + ): ParagraphIntrinsics + + /** Creates a [FontFamily.Resolver] for use outside of composition. */ + fun createFontFamilyResolver(): FontFamily.Resolver + + /** Creates a [FontFamily.Resolver] that loads async fonts on the given [coroutineContext]. */ + fun createFontFamilyResolver(coroutineContext: CoroutineContext): FontFamily.Resolver + + /** Returns the index of the grapheme-cluster break preceding [index] in [text]. */ + fun findPrecedingBreak(text: String, index: Int): Int + + /** Returns the index of the grapheme-cluster break following [index] in [text]. */ + fun findFollowingBreak(text: String, index: Int): Int + + /** + * The platform-default [androidx.compose.ui.text.FontRasterizationSettings], backing + * [androidx.compose.ui.text.FontRasterizationSettings.Companion.PlatformDefault]. The backend computes these from the + * host platform so the platform-specific detection stays out of ui-text. + */ + @OptIn(ExperimentalTextApi::class) + val defaultFontRasterizationSettings: FontRasterizationSettings +} + +@InternalComposeUiApi +object PlatformTextRegistry { + private var implementation: PlatformText? = null + private val lock = SynchronizedObject() + + fun register(implementation: PlatformText) { + synchronized(lock) { + val current = this.implementation + when { + current == null -> this.implementation = implementation + current === implementation -> Unit + else -> error( + "Compose UI text implementation is already registered with a different " + + "instance. Call clear() first if replacement is intentional." + ) + } + } + } + + internal fun requireCurrent(): PlatformText = + implementation ?: error("No Compose UI text implementation is registered.") + + /** + * Clears the current implementation. + * + * Intended for tests or controlled teardown only. + */ + @VisibleForTesting + fun clear() { + implementation = null + } +} diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaMultiParagraphDraw.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkiaMultiParagraphDraw.nonAndroid.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaMultiParagraphDraw.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/SkiaMultiParagraphDraw.nonAndroid.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/Synchronization.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/Synchronization.nonAndroid.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/Synchronization.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/platform/Synchronization.nonAndroid.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/style/LineBreak.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/style/LineBreak.nonAndroid.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/style/LineBreak.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/style/LineBreak.nonAndroid.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/style/TextMotion.skiko.kt b/compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/style/TextMotion.nonAndroid.kt similarity index 100% rename from compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/style/TextMotion.skiko.kt rename to compose/ui/ui-text/src/nonAndroidMain/kotlin/androidx/compose/ui/text/style/TextMotion.nonAndroid.kt diff --git a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/AnnotatedStringTransformSkikoTest.kt b/compose/ui/ui-text/src/nonAndroidTest/kotlin/androidx/compose/ui/text/AnnotatedStringTransformSkikoTest.kt similarity index 100% rename from compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/AnnotatedStringTransformSkikoTest.kt rename to compose/ui/ui-text/src/nonAndroidTest/kotlin/androidx/compose/ui/text/AnnotatedStringTransformSkikoTest.kt diff --git a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/UnresolvedSymbolsRegistryTest.kt b/compose/ui/ui-text/src/nonAndroidTest/kotlin/androidx/compose/ui/text/UnresolvedSymbolsRegistryTest.kt similarity index 100% rename from compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/UnresolvedSymbolsRegistryTest.kt rename to compose/ui/ui-text/src/nonAndroidTest/kotlin/androidx/compose/ui/text/UnresolvedSymbolsRegistryTest.kt diff --git a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/input/ToCharArrayTest.kt b/compose/ui/ui-text/src/nonAndroidTest/kotlin/androidx/compose/ui/text/input/ToCharArrayTest.kt similarity index 100% rename from compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/input/ToCharArrayTest.kt rename to compose/ui/ui-text/src/nonAndroidTest/kotlin/androidx/compose/ui/text/input/ToCharArrayTest.kt diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/ActualParagraph.skiko.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/ActualParagraph.skiko.kt deleted file mode 100644 index 21dca04636023..0000000000000 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/ActualParagraph.skiko.kt +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:JvmMultifileClass -@file:JvmName("ParagraphKt") - -package androidx.compose.ui.text - -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.createFontFamilyResolver -import androidx.compose.ui.text.platform.SkiaParagraphIntrinsics -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.Density -import kotlin.jvm.JvmMultifileClass -import kotlin.jvm.JvmName - -@Suppress("DEPRECATION") -@Deprecated( - "Font.ResourceLoader is deprecated, instead pass FontFamily.Resolver", - replaceWith = - ReplaceWith( - "ActualParagraph(text, style, spanStyles, placeholders, " + - "maxLines, ellipsis, width, density, createFontFamilyResolver(resourceLoader))" - ), -) -actual fun Paragraph( - text: String, - style: TextStyle, - spanStyles: List>, - placeholders: List>, - maxLines: Int, - ellipsis: Boolean, - width: Float, - density: Density, - resourceLoader: Font.ResourceLoader, -): Paragraph = SkiaParagraph( - SkiaParagraphIntrinsics( - text = text, - style = style, - placeholders = placeholders, - annotations = spanStyles, - fontFamilyResolver = createFontFamilyResolver(resourceLoader), - density = density - ), - maxLines, - if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip, - Constraints(maxWidth = width.ceilToInt()), -) - -@Deprecated( - "Paragraph that takes maximum allowed width is deprecated, pass constraints instead.", - ReplaceWith( - "Paragraph(text, style, Constraints(maxWidth = ceil(width).toInt()), density, " + - "fontFamilyResolver, spanStyles, placeholders, maxLines, ellipsis)", - "kotlin.math.ceil", - "androidx.compose.ui.unit.Constraints", - ), -) -actual fun Paragraph( - text: String, - style: TextStyle, - width: Float, - density: Density, - fontFamilyResolver: FontFamily.Resolver, - spanStyles: List>, - placeholders: List>, - maxLines: Int, - ellipsis: Boolean, -): Paragraph = SkiaParagraph( - SkiaParagraphIntrinsics( - text = text, - style = style, - placeholders = placeholders, - annotations = spanStyles, - fontFamilyResolver = fontFamilyResolver, - density = density - ), - maxLines, - if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip, - Constraints(maxWidth = width.ceilToInt()), -) - -@Deprecated( - "Paragraph that takes `ellipsis: Boolean` is deprecated, pass TextOverflow instead.", - level = DeprecationLevel.HIDDEN, -) -actual fun Paragraph( - text: String, - style: TextStyle, - constraints: Constraints, - density: Density, - fontFamilyResolver: FontFamily.Resolver, - spanStyles: List>, - placeholders: List>, - maxLines: Int, - ellipsis: Boolean, -): Paragraph = SkiaParagraph( - SkiaParagraphIntrinsics( - text = text, - style = style, - placeholders = placeholders, - annotations = spanStyles, - fontFamilyResolver = fontFamilyResolver, - density = density - ), - maxLines, - if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip, - constraints -) - -actual fun Paragraph( - text: String, - style: TextStyle, - constraints: Constraints, - density: Density, - fontFamilyResolver: FontFamily.Resolver, - spanStyles: List>, - placeholders: List>, - maxLines: Int, - overflow: TextOverflow, -): Paragraph = SkiaParagraph( - SkiaParagraphIntrinsics( - text = text, - style = style, - placeholders = placeholders, - annotations = spanStyles, - fontFamilyResolver = fontFamilyResolver, - density = density - ), - maxLines, - overflow, - constraints -) - -@Deprecated( - "Paragraph that takes maximum allowed width is deprecated, pass constraints instead.", - ReplaceWith( - "Paragraph(paragraphIntrinsics, Constraints(maxWidth = ceil(width).toInt()), maxLines, " + - "ellipsis)", - "kotlin.math.ceil", - "androidx.compose.ui.unit.Constraints", - ), -) -actual fun Paragraph( - paragraphIntrinsics: ParagraphIntrinsics, - maxLines: Int, - ellipsis: Boolean, - width: Float, -): Paragraph = - SkiaParagraph( - paragraphIntrinsics as SkiaParagraphIntrinsics, - maxLines, - if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip, - Constraints(maxWidth = width.ceilToInt()), - ) - -@Deprecated( - "Paragraph that takes ellipsis: Boolean is deprecated, pass TextOverflow instead.", - level = DeprecationLevel.HIDDEN, -) -actual fun Paragraph( - paragraphIntrinsics: ParagraphIntrinsics, - constraints: Constraints, - maxLines: Int, - ellipsis: Boolean, -): Paragraph = - SkiaParagraph( - paragraphIntrinsics as SkiaParagraphIntrinsics, - maxLines, - if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip, - constraints - ) - -actual fun Paragraph( - paragraphIntrinsics: ParagraphIntrinsics, - constraints: Constraints, - maxLines: Int, - overflow: TextOverflow, -): Paragraph = - SkiaParagraph( - paragraphIntrinsics as SkiaParagraphIntrinsics, - maxLines, - overflow, - constraints - ) diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Paragraph.skiko.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Paragraph.skiko.kt deleted file mode 100644 index b8840076b7e13..0000000000000 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Paragraph.skiko.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.compose.ui.text - -import androidx.annotation.IntRange -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Canvas -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.Shadow -import androidx.compose.ui.graphics.drawscope.DrawStyle -import androidx.compose.ui.text.internal.JvmDefaultWithCompatibility -import androidx.compose.ui.text.style.ResolvedTextDirection -import androidx.compose.ui.text.style.TextDecoration - -@JvmDefaultWithCompatibility -actual sealed interface Paragraph { - actual val width: Float - actual val height: Float - actual val minIntrinsicWidth: Float - actual val maxIntrinsicWidth: Float - actual val firstBaseline: Float - actual val lastBaseline: Float - actual val didExceedMaxLines: Boolean - actual val lineCount: Int - actual val placeholderRects: List - actual fun getPathForRange(start: Int, end: Int): Path - actual fun getCursorRect(offset: Int): Rect - actual fun getLineLeft(lineIndex: Int): Float - actual fun getLineRight(lineIndex: Int): Float - actual fun getLineTop(lineIndex: Int): Float - actual fun getLineBaseline(lineIndex: Int): Float - actual fun getLineBottom(lineIndex: Int): Float - actual fun getLineHeight(lineIndex: Int): Float - actual fun getLineWidth(lineIndex: Int): Float - actual fun getLineStart(lineIndex: Int): Int - actual fun getLineEnd(lineIndex: Int, visibleEnd: Boolean): Int - actual fun isLineEllipsized(lineIndex: Int): Boolean - actual fun getLineForOffset(offset: Int): Int - actual fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float - actual fun getParagraphDirection(offset: Int): ResolvedTextDirection - actual fun getBidiRunDirection(offset: Int): ResolvedTextDirection - actual fun getLineForVerticalPosition(vertical: Float): Int - actual fun getOffsetForPosition(position: Offset): Int - actual fun getRangeForRect( - rect: Rect, - granularity: TextGranularity, - inclusionStrategy: TextInclusionStrategy - ): TextRange - actual fun getBoundingBox(offset: Int): Rect - actual fun fillBoundingBoxes(range: TextRange, array: FloatArray, @IntRange(from = 0) arrayStart: Int) - actual fun getWordBoundary(offset: Int): TextRange - actual fun paint(canvas: Canvas, color: Color, shadow: Shadow?, textDecoration: TextDecoration?) - actual fun paint( - canvas: Canvas, - color: Color, - shadow: Shadow?, - textDecoration: TextDecoration?, - drawStyle: DrawStyle?, - blendMode: BlendMode - ) - actual fun paint( - canvas: Canvas, - brush: Brush, - alpha: Float, - shadow: Shadow?, - textDecoration: TextDecoration?, - drawStyle: DrawStyle?, - blendMode: BlendMode - ) -} \ No newline at end of file diff --git a/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.web.kt b/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.web.kt similarity index 100% rename from compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.skiko.web.kt rename to compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/font/FontFamilyResolver.web.kt diff --git a/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.web.kt b/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.web.kt index d6e3b8b433255..8b6411d7b049f 100644 --- a/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.web.kt +++ b/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/intl/PlatformLocale.web.kt @@ -17,6 +17,7 @@ package androidx.compose.ui.text.intl import androidx.compose.runtime.Immutable +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.util.fastMap import kotlin.js.JsName import kotlin.js.js @@ -68,7 +69,8 @@ private val rtlLanguagesSet = setOf("ar", "fa", "he", "iw", "ji", "ur", "yi") // Implemented according to ComponentOrientation.getOrientation (AWT), // since there is no js API for this. -internal actual fun Locale.isRtl(): Boolean = this.language in rtlLanguagesSet +@InternalComposeUiApi +actual fun Locale.isRtl(): Boolean = this.language in rtlLanguagesSet // K/JS and K/Wasm stdlib doesn't have this type. Therefore, we declare it here. // Ideally it would not be necessary, or at least we would make it internal, but Compose common API diff --git a/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.web.kt b/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.web.kt index a83e02ac3bfc0..47d4bf05b5480 100644 --- a/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.web.kt +++ b/compose/ui/ui-text/src/webMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.web.kt @@ -15,13 +15,15 @@ */ package androidx.compose.ui.text.platform +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontVariation actual sealed class PlatformFont : Font { actual abstract val identity: String actual abstract val variationSettings: FontVariation.Settings - internal actual val cacheKey: String + @InternalComposeUiApi + actual val cacheKey: String // Unlike k/jvm and k/native, `this::class.qualifiedName` API is not available in k/js. // https://youtrack.jetbrains.com/issue/KT-34534 // `class.qualifiedName` is supported in k/wasm since 2.3.0 - https://youtrack.jetbrains.com/issue/KT-69621 diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle index a024083502a80..17019b04068dd 100644 --- a/compose/ui/ui/build.gradle +++ b/compose/ui/ui/build.gradle @@ -206,6 +206,7 @@ androidXMultiplatform { dependsOn(commonMain) dependencies { api(project(":compose:ui:ui-graphics")) + implementation(project(":compose:ui:ui-skiko")) api(project(":compose:ui:ui-text")) api(libs.skiko) implementation(libs.atomicFu) @@ -222,6 +223,7 @@ androidXMultiplatform { // TODO: Move to commonTest? implementation(project(":compose:material:material")) implementation(project(":compose:foundation:foundation")) + implementation(project(":compose:ui:ui-skiko")) implementation(project(":compose:ui:ui-test")) } } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt index 7b19f15a36261..035548b631452 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformWindowContext +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.scene.skia.SkiaLayerComponent import androidx.compose.ui.skiko.OverlayRenderDecorator import androidx.compose.ui.unit.Constraints @@ -102,6 +103,12 @@ internal class ComposeContainer( coroutineContext: CoroutineContext = EmptyCoroutineContext, ) : WindowFocusListener, WindowListener { + // Register before any property initializer below (e.g. [mediator]) touches the Skiko backend, + // so every desktop entry point (windows, panels, dialogs) is covered. + init { + registerSkikoComposeImplementation() + } + val windowContext = PlatformWindowContext() var window: Window? = null private set diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicLayerBugTest.desktop.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicLayerBugTest.desktop.kt index dd1e257dc3df6..7df5441ea97c8 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicLayerBugTest.desktop.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicLayerBugTest.desktop.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.scene.CanvasLayersComposeScene import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.unit.dp @@ -90,6 +91,7 @@ class GraphicLayerBugDesktopTest { coroutineException = throwable } ) { + registerSkikoComposeImplementation() val frameRecomposer = FrameRecomposer(coroutineContext) val scene = CanvasLayersComposeScene( frameRecomposer = frameRecomposer, diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt index d501353734333..9414aef37295e 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt @@ -62,6 +62,11 @@ internal class RenderingTestScope( val surface: Surface = Surface.makeRasterN32Premul(width, height) private val canvas = surface.canvas.asComposeCanvas() + + init { + registerSkikoComposeImplementation() + } + val scene = CanvasLayersComposeScene( frameRecomposer = frameRecomposer, invalidateLayout = sceneRenderingScope::onSceneInvalidation, diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt index 2e42101d76d8f..0bbbc399cb171 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.platform.FrameRecomposer import androidx.compose.ui.platform.MotionDurationScaleImpl import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformWindowContext +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.uikit.ComposeContainerConfiguration import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalUIViewController @@ -79,6 +80,11 @@ internal class ComposeContainer( private val coroutineContext: CoroutineContext, private val lifecycleDelegate: ComposeContainerLifecycleDelegate ) { + // Register before any property initializer / scene setup below touches the Skiko backend, so + // every iOS entry point (ComposeHostingView, ComposeHostingViewController) is covered. + init { + registerSkikoComposeImplementation() + } val view = ComposeContainerView( transparentForTouches = false, diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/ComposeUIViewController.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/ComposeUIViewController.ios.kt index 399f1973ddec7..7c4e005a6fc95 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/ComposeUIViewController.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/ComposeUIViewController.ios.kt @@ -47,7 +47,9 @@ fun ComposeUIViewController(content: @Composable () -> Unit): UIViewController = fun ComposeUIViewController( configure: ComposeUIViewControllerConfiguration.() -> Unit = {}, content: @Composable () -> Unit -): UIViewController = ComposeHostingViewController( - configuration = ComposeUIViewControllerConfiguration().apply(configure), - content = content, -) +): UIViewController { + return ComposeHostingViewController( + configuration = ComposeUIViewControllerConfiguration().apply(configure), + content = content, + ) +} diff --git a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt index aa6ebc2aa3349..983f75f27ffc7 100644 --- a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt +++ b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner import androidx.compose.ui.platform.MacosTextInputService import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.WindowInfoImpl +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.scene.CanvasLayersComposeScene import androidx.compose.ui.scene.SingleComposeSceneRenderingScope import androidx.compose.ui.platform.FrameRecomposer @@ -122,6 +123,11 @@ private class ComposeWindow( } } private val skiaLayer = SkiaLayer() + + init { + registerSkikoComposeImplementation() + } + private val scene = CanvasLayersComposeScene( frameRecomposer = frameRecomposer, platformContext = platformContext, diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ImageComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ImageComposeScene.skiko.kt index 4d5d46c07cb8b..45df6d52f99bf 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ImageComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ImageComposeScene.skiko.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.platform.WindowInfoImpl +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.scene.CanvasLayersComposeScene import androidx.compose.ui.platform.FrameRecomposer import androidx.compose.ui.scene.ComposeScene @@ -186,6 +187,10 @@ class ImageComposeScene @ExperimentalComposeUiApi constructor( get() = _windowInfo } + init { + registerSkikoComposeImplementation() + } + private val scene = CanvasLayersComposeScene( frameRecomposer = frameRecomposer, density = density, diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/GraphicsLayerOwnerLayer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/GraphicsLayerOwnerLayer.skiko.kt index 3a65048b21354..3fa7b1e0f349c 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/GraphicsLayerOwnerLayer.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/GraphicsLayerOwnerLayer.skiko.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.graphics.isIdentity import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.graphics.layer.setOutline -import androidx.compose.ui.graphics.prepareTransformationMatrix import androidx.compose.ui.internal.checkPreconditionNotNull import androidx.compose.ui.internal.requirePrecondition import androidx.compose.ui.platform.invertTo diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/LegacyRenderNodeLayer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/LegacyRenderNodeLayer.skiko.kt index b58f8e75ec59b..0953ecaf8c99e 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/LegacyRenderNodeLayer.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/LegacyRenderNodeLayer.skiko.kt @@ -40,11 +40,10 @@ import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.alphaMultiplier import androidx.compose.ui.graphics.asComposeCanvas import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.materializeSkiaPath import androidx.compose.ui.graphics.skiaCanvas -import androidx.compose.ui.graphics.skiaPaint -import androidx.compose.ui.graphics.prepareTransformationMatrix import androidx.compose.ui.graphics.skiaImageFilter -import androidx.compose.ui.graphics.materializeSkiaPath +import androidx.compose.ui.graphics.skiaPaint import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.invertTo import androidx.compose.ui.platform.isInOutline diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/Matrices.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/Matrices.skiko.kt new file mode 100644 index 0000000000000..4bf2ac549d3e9 --- /dev/null +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/Matrices.skiko.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.node + +import androidx.compose.ui.graphics.Matrix +import kotlin.math.abs + +internal fun prepareTransformationMatrix( + matrix: Matrix, + pivotX: Float, + pivotY: Float, + translationX: Float, + translationY: Float, + rotationX: Float, + rotationY: Float, + rotationZ: Float, + scaleX: Float, + scaleY: Float, + cameraDistance: Float, +) { + matrix.reset() + matrix.translate(x = -pivotX, y = -pivotY) + matrix *= Matrix().apply { + rotateZ(rotationZ) + rotateY(rotationY) + rotateX(rotationX) + scale(scaleX, scaleY) + } + // Perspective transform should be applied only in case of rotations to avoid + // multiply application in hierarchies. + // See Android's frameworks/base/libs/hwui/RenderProperties.cpp for reference + if (!rotationX.isZero() || !rotationY.isZero()) { + matrix *= Matrix().apply { + // The camera location is passed in inches, set in pt + val depth = cameraDistance * 72f + this[2, 3] = -1f / depth + } + } + matrix *= Matrix().apply { + translate(x = pivotX + translationX, y = pivotY + translationY) + } + + // Third column and row are irrelevant for 2D space. + // Zeroing required to get correct inverse transformation matrix. + matrix[2, 0] = 0f + matrix[2, 1] = 0f + matrix[2, 3] = 0f + matrix[0, 2] = 0f + matrix[1, 2] = 0f + matrix[3, 2] = 0f +} + +// Copy from Android's frameworks/base/libs/hwui/utils/MathUtils.h +private const val NON_ZERO_EPSILON = 0.001f + +@Suppress("NOTHING_TO_INLINE") +private inline fun Float.isZero(): Boolean = abs(this) <= NON_ZERO_EPSILON diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt index fd030804235ac..1461e3f7bd743 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt @@ -197,7 +197,7 @@ internal class RootNodeOwner( coroutineScope.cancel() platformContext.rootForTestListener?.onRootForTestDisposed(rootForTest) snapshotObserver.stopObserving() - graphicsContext.dispose() + graphicsContext.close() _owner.dispose() // we don't need to call root.detach() because root will be garbage collected isDisposed = true diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt index 8a866f4a89d1c..1690d02535c19 100644 --- a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.isEqualTo import androidx.compose.ui.platform.FrameRecomposer import androidx.compose.ui.platform.LocalPointerIconService import androidx.compose.ui.platform.PlatformContext +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.scene.ComposeSceneContext import androidx.compose.ui.scene.PlatformLayersComposeScene @@ -364,6 +365,7 @@ private fun createPlatformLayersScene( platformContext: PlatformContext, invalidate: () -> Unit = {}, ): Pair { + registerSkikoComposeImplementation() val frameRecomposer = FrameRecomposer(coroutineContext, invalidate) val scene = PlatformLayersComposeScene( frameRecomposer = frameRecomposer, diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/OwnerLayerTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/OwnerLayerTest.kt index 9912e9b026ee4..17b12af2d9648 100644 --- a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/OwnerLayerTest.kt +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/OwnerLayerTest.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.snapshots.SnapshotStateObserver +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.isFinite @@ -42,6 +43,7 @@ import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.asComposeCanvas import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.platform.invertTo +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.unit.Density @@ -56,8 +58,13 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue import org.jetbrains.skia.Surface +@OptIn(InternalComposeUiApi::class) class OwnerLayerTest { + init { + registerSkikoComposeImplementation() + } + private val layer = TestRenderNodeLayer() private val cos45 = cos(PI / 4).toFloat() diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/RootNodeOwnerTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/RootNodeOwnerTest.kt index 39cbbf6c41580..3139a3284a728 100644 --- a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/RootNodeOwnerTest.kt +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/RootNodeOwnerTest.kt @@ -16,6 +16,9 @@ package androidx.compose.ui.node +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import kotlin.test.BeforeTest import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect @@ -45,8 +48,22 @@ import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest +@OptIn(InternalComposeUiApi::class) class RootNodeOwnerTest { + @BeforeTest + fun registerSkikoBackend() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once tests that register the backend asynchronously + // (e.g. AWT ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on + // the registration persisting across tests. + // @AfterTest + // fun clearSkikoBackend() { + // clearSkikoComposeImplementation() + // } + @Test fun textTextInputSession() = runTest { var sessionStarted = false diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/VoteFrameRateTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/VoteFrameRateTest.kt index 7dfb9d31a97d1..9272eca1baaf2 100644 --- a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/VoteFrameRateTest.kt +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/node/VoteFrameRateTest.kt @@ -16,6 +16,9 @@ package androidx.compose.ui.node +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import kotlin.test.BeforeTest import androidx.compose.ui.FrameRateCategory import androidx.compose.ui.graphics.asComposeCanvas import androidx.compose.ui.platform.PlatformContext @@ -31,8 +34,22 @@ import kotlin.test.fail import kotlinx.coroutines.test.runTest import org.jetbrains.skia.Surface +@OptIn(InternalComposeUiApi::class) class VoteFrameRateTest { + @BeforeTest + fun registerSkikoBackend() { + registerSkikoComposeImplementation() + } + + // TODO: re-enable per-test cleanup once tests that register the backend asynchronously + // (e.g. AWT ComposePanel/ComposeWindow via ComposeContainer on the EDT) no longer rely on + // the registration persisting across tests. + // @AfterTest + // fun clearSkikoBackend() { + // clearSkikoComposeImplementation() + // } + @Test fun testNoVotedFrameRate() = runTest { val surface = Surface.makeRasterN32Premul(100, 100) diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/scene/BaseComposeSceneTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/scene/BaseComposeSceneTest.kt index 4354a6d7aecd1..8bc0e4363345a 100644 --- a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/scene/BaseComposeSceneTest.kt +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/scene/BaseComposeSceneTest.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.PointerInputModifierNode import androidx.compose.ui.platform.FrameRecomposer +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.unit.IntSize import kotlin.coroutines.CoroutineContext import kotlin.test.Test @@ -161,6 +162,7 @@ private fun createPlatformLayersScene( invalidateLayout: () -> Unit = {}, invalidateDraw: () -> Unit = {}, ): Pair { + registerSkikoComposeImplementation() val frameRecomposer = FrameRecomposer(coroutineContext) val scene = PlatformLayersComposeScene( frameRecomposer = frameRecomposer, @@ -180,6 +182,7 @@ private fun createCanvasLayersScene( invalidateLayout: () -> Unit = {}, invalidateDraw: () -> Unit = {}, ): Pair { + registerSkikoComposeImplementation() val frameRecomposer = FrameRecomposer(coroutineContext) val scene = CanvasLayersComposeScene( frameRecomposer = frameRecomposer, diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/scene/CanvasLayersComposeSceneTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/scene/CanvasLayersComposeSceneTest.kt index 50c8cc8618428..4d717757836e8 100644 --- a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/scene/CanvasLayersComposeSceneTest.kt +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/scene/CanvasLayersComposeSceneTest.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.platform.FrameRecomposer +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.unit.IntSize import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -37,6 +38,7 @@ class CanvasLayersComposeSceneTest { @Test fun sceneSizeChangeTriggersInvalidation() = runTest(StandardTestDispatcher()) { + registerSkikoComposeImplementation() var invalidationCount = 0 val frameRecomposer = FrameRecomposer(coroutineContext) CanvasLayersComposeScene( @@ -56,6 +58,7 @@ class CanvasLayersComposeSceneTest { @Test fun cancelClickForGestureOwner() = runTest(StandardTestDispatcher()) { + registerSkikoComposeImplementation() var rootCancelled = false var popupCancelled = false val frameRecomposer = FrameRecomposer(coroutineContext) diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/window/PopupTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/window/PopupTest.kt index 525795add4bb3..f2ceb685ce732 100644 --- a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/window/PopupTest.kt +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/window/PopupTest.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformInsets import androidx.compose.ui.platform.PlatformWindowInsets import androidx.compose.ui.platform.WindowInfoImpl +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.platform.testTag import androidx.compose.ui.scene.CanvasLayersComposeScene import androidx.compose.ui.platform.FrameRecomposer @@ -812,6 +813,7 @@ class PopupTest { } error("ComposeScene did not become idle") } + registerSkikoComposeImplementation() scene = CanvasLayersComposeScene( frameRecomposer = frameRecomposer, platformContext = PlatformContext.Empty().also { diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorTest.kt index 12cb556d6bcee..13308c0d89b5c 100644 --- a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorTest.kt +++ b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorTest.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.navigationevent.UIKitNavigationEventInput import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner import androidx.compose.ui.platform.PlatformWindowContext +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.scene.ComposeSceneContext import androidx.compose.ui.scene.ComposeSceneMediator import androidx.compose.ui.scene.PlatformLayersComposeScene @@ -112,6 +113,7 @@ class ComposeSceneMediatorTest { ), interfaceOrientationState = mutableStateOf(InterfaceOrientation.Portrait), composeSceneFactory = { invalidate, platformContext, frameRecomposer -> + registerSkikoComposeImplementation() PlatformLayersComposeScene( frameRecomposer = frameRecomposer, density = Density(1f), diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/test/UIKitInstrumentedTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/test/UIKitInstrumentedTest.kt index 0ffb2ba5fdb90..5efd7c0bfc048 100644 --- a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/test/UIKitInstrumentedTest.kt +++ b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/test/UIKitInstrumentedTest.kt @@ -18,6 +18,7 @@ package androidx.compose.ui.test import androidx.compose.runtime.Composable import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.platform.AccessibilityNotification import androidx.compose.ui.platform.InfiniteAnimationPolicy import androidx.compose.ui.scene.ComposeHostingView @@ -191,7 +192,7 @@ internal fun runUIKitInstrumentedTest( * Constructor properties are initialized with the attributes of the main screen and a mock delegate to simulate * the application setup. */ -@OptIn(ExperimentalForeignApi::class) +@OptIn(ExperimentalForeignApi::class, InternalComposeUiApi::class) internal class UIKitInstrumentedTest( private val useHostingView: Boolean ) { diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt index 88e2b0139c056..bcaadb8c3ba8c 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt @@ -63,6 +63,7 @@ import androidx.compose.ui.platform.WebTextInputService import androidx.compose.ui.platform.WebTextToolbar import androidx.compose.ui.platform.WebWakeLockManager import androidx.compose.ui.platform.WindowInfoImpl +import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.platform.accessibility.ComposeWebSemanticsListener import androidx.compose.ui.platform.installFallbackFontDownloader import androidx.compose.ui.scene.CanvasLayersComposeScene @@ -347,6 +348,10 @@ internal class ComposeWindow( } } + init { + registerSkikoComposeImplementation() + } + private val scene = CanvasLayersComposeScene( frameRecomposer = frameRecomposer, platformContext = platformContext, diff --git a/settings.gradle b/settings.gradle index 22cf9a681236e..8eeba8a9c5b3a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -504,6 +504,7 @@ includeProject(":compose:ui:ui-graphics") includeProject(":compose:ui:ui-graphics-lint") includeProject(":compose:ui:ui-graphics:ui-graphics-samples", "compose/ui/ui-graphics/samples") includeProject(":compose:ui:ui-lint") +includeProject(":compose:ui:ui-skiko") includeProject(":compose:ui:ui-test") includeProject(":compose:ui:ui-test:ui-test-samples", "compose/ui/ui-test/samples") includeProject(":compose:ui:ui-test-junit4")