diff --git a/compose/ui/ui/api/desktop/ui.api b/compose/ui/ui/api/desktop/ui.api index 08b6831f51532..1cd33f06370bd 100644 --- a/compose/ui/ui/api/desktop/ui.api +++ b/compose/ui/ui/api/desktop/ui.api @@ -457,7 +457,8 @@ public final class androidx/compose/ui/awt/ComposeDialog : javax/swing/JDialog { public final class androidx/compose/ui/awt/ComposePanel : javax/swing/JLayeredPane { public static final field $stable I public fun ()V - public fun (Lorg/jetbrains/skiko/SkiaLayerAnalytics;)V + public fun (Lorg/jetbrains/skiko/SkiaLayerAnalytics;Landroidx/compose/ui/awt/RenderSettings;)V + public synthetic fun (Lorg/jetbrains/skiko/SkiaLayerAnalytics;Landroidx/compose/ui/awt/RenderSettings;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun add (Ljava/awt/Component;)Ljava/awt/Component; public fun addFocusListener (Ljava/awt/event/FocusListener;)V public fun addNotify ()V @@ -528,6 +529,17 @@ public final class androidx/compose/ui/awt/ComposeWindow : javax/swing/JFrame { public fun setUndecorated (Z)V } +public final class androidx/compose/ui/awt/RenderSettings { + public static final field $stable I + public static final field Companion Landroidx/compose/ui/awt/RenderSettings$Companion; + public fun (Ljava/lang/Boolean;)V + public final fun isVsyncEnabled ()Ljava/lang/Boolean; +} + +public final class androidx/compose/ui/awt/RenderSettings$Companion { + public final fun getDefault ()Landroidx/compose/ui/awt/RenderSettings; +} + public final class androidx/compose/ui/awt/SwingPanel_desktopKt { public static final fun SwingPanel-euL9pac (JLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V public static final fun getNoOpUpdate ()Lkotlin/jvm/functions/Function1; diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt index 84fdfabe1fd83..9e0900b48ee22 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt @@ -42,11 +42,13 @@ import org.jetbrains.skiko.SkiaLayerAnalytics * @param skiaLayerAnalytics Analytics that helps to know more about SkiaLayer behaviour. * SkiaLayer is underlying class used internally to draw Compose content. * Implementation usually uses third-party solution to send info to some centralized analytics gatherer. + * @param renderSettings Configuration class for rendering settings. */ class ComposePanel @ExperimentalComposeUiApi constructor( private val skiaLayerAnalytics: SkiaLayerAnalytics, + private val renderSettings: RenderSettings = RenderSettings.Default ) : JLayeredPane() { - constructor() : this(SkiaLayerAnalytics.Empty) + constructor() : this(SkiaLayerAnalytics.Empty, RenderSettings.Default) init { check(isEventDispatchThread()) { @@ -196,7 +198,8 @@ class ComposePanel @ExperimentalComposeUiApi constructor( return ComposeContainer( container = this, skiaLayerAnalytics = skiaLayerAnalytics, - windowContainer = windowContainer + windowContainer = windowContainer, + renderSettings = renderSettings, ).apply { focusManager.releaseFocus() setBounds(0, 0, width, height) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt index 15ce403f58803..ee756bc7848f7 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt @@ -59,7 +59,9 @@ internal class ComposeWindowPanel( // but it's always disabled here. Using fallback instead of [check] to support // opening separate windows from [ComposePanel] with such layer type. if (it == LayerType.OnComponent) LayerType.OnSameCanvas else it - } + }, + // TODO: Add RenderingSettings to ComposeWindowPanel constructor + renderSettings = RenderSettings.Default ) private val composeContainer get() = requireNotNull(_composeContainer) { diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/RenderingSettings.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/RenderingSettings.desktop.kt new file mode 100644 index 0000000000000..f7bd2f2173d5e --- /dev/null +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/RenderingSettings.desktop.kt @@ -0,0 +1,50 @@ +/* + * 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.awt + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.ComposeFeatureFlags +import org.jetbrains.skiko.SkikoProperties + +/** + * Configuration class for rendering settings. + * + * @property isVsyncEnabled Indicates whether presentation vsync is enabled or not + * When true, the internal implementation will attempt to synchronize drawable presentations + * with vsync. It guarantees that the frame is presented without visual artifacts like tearing in + * exchange for a latency increase. + * When false, the internal implementation will not attempt to synchronize drawable presentations + * it can reduce latency but may introduce visual artifacts like screen tearing. + * When null, the internal implementation will use the global configuration from [SkikoProperties.vsyncEnabled] + * + * This flag has no effect if [ComposeFeatureFlags.useSwingGraphics] is true. In this case Compose + * will render the content to Swing provided offscreen buffer and the presentation will be controlled + * by Swing. + */ +@ExperimentalComposeUiApi +class RenderSettings( + val isVsyncEnabled: Boolean? +) { + companion object { + /** + * Default rendering settings + */ + val Default = RenderSettings( + isVsyncEnabled = null + ) + } +} \ No newline at end of file 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 addcb32c9b92b..6def8254785db 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 @@ -26,6 +26,7 @@ import androidx.compose.ui.LayerType import androidx.compose.ui.awt.AwtEventFilter import androidx.compose.ui.awt.AwtEventListener import androidx.compose.ui.awt.AwtEventListeners +import androidx.compose.ui.awt.RenderSettings import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalInternalViewModelStoreOwner @@ -78,6 +79,7 @@ import org.jetbrains.skiko.SkiaLayerAnalytics * for window coordinate space. * @property useSwingGraphics Flag indicating if offscreen rendering to Swing graphics is used. * @property layerType The type of layer used for Popup/Dialog. + * @property renderSettings The settings for rendering. */ internal class ComposeContainer( val container: JLayeredPane, @@ -88,6 +90,7 @@ internal class ComposeContainer( private val useSwingGraphics: Boolean = ComposeFeatureFlags.useSwingGraphics, private val layerType: LayerType = ComposeFeatureFlags.layerType, + private val renderSettings: RenderSettings, ) : WindowFocusListener, WindowListener, LifecycleOwner, ViewModelStoreOwner { val windowContext = PlatformWindowContext() var window: Window? = null @@ -338,7 +341,13 @@ internal class ComposeContainer( return if (useSwingGraphics) { SwingSkiaLayerComponent(mediator, renderDelegate, skiaLayerAnalytics) } else { - WindowSkiaLayerComponent(mediator, windowContext, renderDelegate, skiaLayerAnalytics) + WindowSkiaLayerComponent( + mediator, + windowContext, + renderDelegate, + skiaLayerAnalytics, + renderSettings + ) } } @@ -381,7 +390,8 @@ internal class ComposeContainer( density = density, layoutDirection = layoutDirection, focusable = focusable, - compositionContext = compositionContext + compositionContext = compositionContext, + renderSettings = renderSettings ) LayerType.OnComponent -> SwingComposeSceneLayer( composeContainer = this, diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/WindowComposeSceneLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/WindowComposeSceneLayer.desktop.kt index 0e319760a2215..00631cae39d79 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/WindowComposeSceneLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/WindowComposeSceneLayer.desktop.kt @@ -18,6 +18,7 @@ package androidx.compose.ui.scene import org.jetbrains.skia.Rect as SkRect import androidx.compose.runtime.CompositionContext +import androidx.compose.ui.awt.RenderSettings import androidx.compose.ui.awt.getTransparentWindowBackground import androidx.compose.ui.awt.setTransparent import androidx.compose.ui.awt.toAwtRectangle @@ -52,7 +53,8 @@ internal class WindowComposeSceneLayer( density: Density, layoutDirection: LayoutDirection, focusable: Boolean, - compositionContext: CompositionContext + compositionContext: CompositionContext, + private val renderSettings: RenderSettings ) : DesktopComposeSceneLayer(composeContainer, density, layoutDirection) { private val window get() = requireNotNull(composeContainer.window) private val windowContext = PlatformWindowContext().also { @@ -193,7 +195,8 @@ internal class WindowComposeSceneLayer( mediator = mediator, windowContext = windowContext, renderDelegate = renderDelegate, - skiaLayerAnalytics = skiaLayerAnalytics + skiaLayerAnalytics = skiaLayerAnalytics, + renderSettings = renderSettings, ) } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/skia/WindowSkiaLayerComponent.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/skia/WindowSkiaLayerComponent.desktop.kt index 97802daa8804f..199f2df501bea 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/skia/WindowSkiaLayerComponent.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/skia/WindowSkiaLayerComponent.desktop.kt @@ -16,6 +16,7 @@ package androidx.compose.ui.scene.skia +import androidx.compose.ui.awt.RenderSettings import androidx.compose.ui.platform.PlatformWindowContext import androidx.compose.ui.scene.ComposeSceneMediator import java.awt.Dimension @@ -24,6 +25,7 @@ import javax.accessibility.Accessible import org.jetbrains.skiko.GraphicsApi import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayerAnalytics +import org.jetbrains.skiko.SkiaLayerProperties import org.jetbrains.skiko.SkikoRenderDelegate /** @@ -36,7 +38,8 @@ internal class WindowSkiaLayerComponent( private val mediator: ComposeSceneMediator, private val windowContext: PlatformWindowContext, renderDelegate: SkikoRenderDelegate, - skiaLayerAnalytics: SkiaLayerAnalytics + skiaLayerAnalytics: SkiaLayerAnalytics, + private val renderSettings: RenderSettings, ) : SkiaLayerComponent { /** * See also backend layer for swing interop in [SwingSkiaLayerComponent] @@ -47,6 +50,13 @@ internal class WindowSkiaLayerComponent( // apply `checkNotNull` for "non-null" field. checkNotNull(mediator.accessible) }, + properties = run { + val defaultProperties = SkiaLayerProperties() + + SkiaLayerProperties( + isVsyncEnabled = renderSettings.isVsyncEnabled ?: defaultProperties.isVsyncEnabled, + ) + }, analytics = skiaLayerAnalytics ) { override fun paint(g: Graphics) { diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposePanelTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposePanelTest.kt index a9289d0299522..42ba7ba0105bc 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposePanelTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposePanelTest.kt @@ -177,7 +177,10 @@ class ComposePanelTest { } } - val composePanel = ComposePanel(skiaLayerAnalytics = analytics) + val composePanel = ComposePanel( + skiaLayerAnalytics = analytics, + renderSettings = RenderSettings.Default + ) composePanel.size = Dimension(100, 100) val frame = JFrame() diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/ComposeContainerLifecycleOwnerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/ComposeContainerLifecycleOwnerTest.kt index e5ba6eea617af..ee8d43ea2e7cb 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/ComposeContainerLifecycleOwnerTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/ComposeContainerLifecycleOwnerTest.kt @@ -17,13 +17,12 @@ package androidx.compose.ui.window import androidx.compose.ui.assertThat +import androidx.compose.ui.awt.RenderSettings import androidx.compose.ui.isEqualTo import androidx.compose.ui.scene.ComposeContainer import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner -import java.awt.Frame -import java.awt.Toolkit import java.time.Duration import javax.swing.JFrame import javax.swing.JLayeredPane @@ -142,7 +141,8 @@ class ComposeContainerLifecycleOwnerTest { container = ComposeContainer( container = pane, skiaLayerAnalytics = SkiaLayerAnalytics.Empty, - window = window + window = window, + renderSettings = RenderSettings.Default ).also { it.lifecycle.addObserver(allEvents) } @@ -172,7 +172,8 @@ class ComposeContainerLifecycleOwnerTest { container = ComposeContainer( container = pane, skiaLayerAnalytics = SkiaLayerAnalytics.Empty, - window = window + window = window, + renderSettings = RenderSettings.Default ).also { it.lifecycle.addObserver(allEvents) } @@ -202,7 +203,8 @@ class ComposeContainerLifecycleOwnerTest { container = ComposeContainer( container = pane, skiaLayerAnalytics = SkiaLayerAnalytics.Empty, - window = window + window = window, + renderSettings = RenderSettings.Default ).also { it.lifecycle.addObserver(allEvents) } @@ -250,7 +252,8 @@ class ComposeContainerLifecycleOwnerTest { container = ComposeContainer( container = this, skiaLayerAnalytics = SkiaLayerAnalytics.Empty, - window = window + window = window, + renderSettings = RenderSettings.Default ) container.lifecycle.addObserver(observer) window.add(this) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d60db54f975c..98af89de2f017 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,7 +54,7 @@ moshi = "1.13.0" protobuf = "3.21.8" paparazzi = "1.0.0" paparazziNative = "2022.1.1-canary-f5f9f71" -skiko = "0.8.4" +skiko = "0.8.8" sqldelight = "1.3.0" retrofit = "2.7.2" wire = "4.5.1"