Merge "ui-desktop: different redrawing strategy with DesktopUIDispatcher" into androidx-master-dev
diff --git a/compose/compose-dispatch/src/desktopMain/kotlin/androidx/compose/dispatch/ActualDesktop.kt b/compose/compose-dispatch/src/desktopMain/kotlin/androidx/compose/dispatch/ActualDesktop.kt
index c5ec776..baa6d0f 100644
--- a/compose/compose-dispatch/src/desktopMain/kotlin/androidx/compose/dispatch/ActualDesktop.kt
+++ b/compose/compose-dispatch/src/desktopMain/kotlin/androidx/compose/dispatch/ActualDesktop.kt
@@ -16,24 +16,6 @@
package androidx.compose.dispatch
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.withContext
-
-/**
- * TODO: a more appropriate implementation that matches the vsync rate of the default display.
- * The current implementation will result in clock skew over time as resuming from delay() is not
- * guaranteed to be precise or frame-accurate.
- */
-private object MainDispatcherFrameClock : MonotonicFrameClock {
- private const val DefaultFrameDelay = 16L // milliseconds
-
- override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R =
- withContext(Dispatchers.Main) {
- delay(DefaultFrameDelay)
- onFrame(System.nanoTime())
- }
+actual val DefaultMonotonicFrameClock: MonotonicFrameClock by lazy {
+ DesktopUiDispatcher.Dispatcher.frameClock
}
-
-actual val DefaultMonotonicFrameClock: MonotonicFrameClock
- get() = MainDispatcherFrameClock
diff --git a/compose/compose-dispatch/src/desktopMain/kotlin/androidx/compose/dispatch/DesktopUiDispatcher.kt b/compose/compose-dispatch/src/desktopMain/kotlin/androidx/compose/dispatch/DesktopUiDispatcher.kt
new file mode 100644
index 0000000..5b7082e
--- /dev/null
+++ b/compose/compose-dispatch/src/desktopMain/kotlin/androidx/compose/dispatch/DesktopUiDispatcher.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.dispatch
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Runnable
+import kotlinx.coroutines.suspendCancellableCoroutine
+import java.awt.event.ActionListener
+import javax.swing.SwingUtilities.invokeLater
+import javax.swing.Timer
+import kotlin.coroutines.CoroutineContext
+
+private typealias Action = (Long) -> Unit
+private typealias Queue = ArrayList<Action>
+
+/**
+ * Ticks scheduler for Desktop. It tries to mimic Android's Choreographer and has multiple levels of
+ * callbacks: "just" callbacks and "after" callbacks (Choreographer has more, but two is enough for us)
+ * It's necessary because recomposition and drawing should be synchronized, otherwise races and
+ * inconsistencies are possible. On Android, this synchronization is achieved implicitly by executing
+ * recomposition and scheduling of redrawing as different types of callbacks. It guarantees,
+ * that after recomposition, redrawing happens in the exactly same tick.
+ *
+ * There are some plans to make possible redrawing based on composition snapshots,
+ * so maybe some requirements for dispatcher will be mitigated in the future.
+ **/
+class DesktopUiDispatcher : CoroutineDispatcher() {
+ private val lock = Any()
+ private var callbacks = Queue()
+ private var afterCallbacks = Queue()
+
+ @Volatile
+ private var scheduled = false
+
+ private fun scheduleIfNeeded() {
+ synchronized(lock) {
+ if (!scheduled && (callbacks.isNotEmpty() || afterCallbacks.isNotEmpty())) {
+ invokeLater { tick() }
+ scheduled = true
+ }
+ }
+ }
+
+ val frameClock: MonotonicFrameClock = object :
+ MonotonicFrameClock {
+ override suspend fun <R> withFrameNanos(
+ onFrame: (Long) -> R
+ ): R {
+ return suspendCancellableCoroutine { co ->
+ val callback = { now: Long ->
+ val res = runCatching {
+ onFrame(now)
+ }
+ co.resumeWith(res)
+ }
+ scheduleCallback(callback)
+ co.invokeOnCancellation {
+ removeCallback(callback)
+ }
+ }
+ }
+ }
+
+ private fun tick() {
+ scheduled = false
+ val now = System.nanoTime()
+ runCallbacks(now, callbacks)
+ runCallbacks(now, afterCallbacks)
+ scheduleIfNeeded()
+ }
+
+ fun scheduleCallback(action: Action) {
+ synchronized(lock) {
+ callbacks.add(action)
+ scheduleIfNeeded()
+ }
+ }
+
+ fun scheduleAfterCallback(action: Action) {
+ synchronized(lock) {
+ afterCallbacks.add(action)
+ }
+ }
+
+ fun removeCallback(action: (Long) -> Unit) {
+ synchronized(lock) {
+ callbacks.remove(action)
+ }
+ }
+
+ fun scheduleCallbackWithDelay(delay: Int, action: Action) {
+ Timer(delay,
+ ActionListener {
+ scheduleCallback { action(System.nanoTime()) }
+ }).apply {
+ isRepeats = false
+ start()
+ }
+ }
+
+ private fun runCallbacks(now: Long, callbacks: Queue) {
+ synchronized(lock) {
+ callbacks.toList().also { callbacks.clear() }
+ }.forEach { it(now) }
+ }
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ scheduleCallback { block.run() }
+ }
+
+ companion object {
+ val Dispatcher: DesktopUiDispatcher by lazy { DesktopUiDispatcher() }
+ val Main: CoroutineContext by lazy {
+ Dispatcher + Dispatcher.frameClock
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/compose-runtime/src/desktopMain/kotlin/androidx/compose/ActualDesktop.kt b/compose/compose-runtime/src/desktopMain/kotlin/androidx/compose/ActualDesktop.kt
index 9a30d380..2afaf2d 100644
--- a/compose/compose-runtime/src/desktopMain/kotlin/androidx/compose/ActualDesktop.kt
+++ b/compose/compose-runtime/src/desktopMain/kotlin/androidx/compose/ActualDesktop.kt
@@ -18,7 +18,7 @@
import javax.swing.SwingUtilities
import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.Dispatchers
+import androidx.compose.dispatch.DesktopUiDispatcher
// API to allow override embedding context creation mechanism for tests.
var EmbeddingContextFactory: (() -> EmbeddingContext)? = null
@@ -29,19 +29,19 @@
}
override fun mainThreadCompositionContext(): CoroutineContext {
- return Dispatchers.Main
+ return DesktopUiDispatcher.Main
}
override fun postOnMainThread(block: () -> Unit) {
- SwingUtilities.invokeLater(block)
+ DesktopUiDispatcher.Dispatcher.scheduleCallback { block() }
}
private val cancelled = mutableSetOf<ChoreographerFrameCallback>()
override fun postFrameCallback(callback: ChoreographerFrameCallback) {
- postOnMainThread {
+ DesktopUiDispatcher.Dispatcher.scheduleCallback { now ->
if (callback !in cancelled) {
- callback.doFrame(System.currentTimeMillis() * 1000000)
+ callback.doFrame(now)
} else {
cancelled.remove(callback)
}
diff --git a/ui/ui-desktop/android-emu/src/desktopMain/kotlin/android/view/View.kt b/ui/ui-desktop/android-emu/src/desktopMain/kotlin/android/view/View.kt
index d85749e..6a2afee 100644
--- a/ui/ui-desktop/android-emu/src/desktopMain/kotlin/android/view/View.kt
+++ b/ui/ui-desktop/android-emu/src/desktopMain/kotlin/android/view/View.kt
@@ -73,7 +73,9 @@
return false
}
- open fun invalidate() {}
+ open fun invalidate() {
+ onInvalidate()
+ }
open fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {}
@@ -199,10 +201,21 @@
}
}
+ open fun offsetLeftAndRight(offset: Int) {
+ if (offset != 0) {
+ left += offset
+ right += offset
+ }
+ }
+
open fun onAttachedToWindow() {
isAttachedToWindow = true
attachListeners.forEach {
it.onViewAttachedToWindow(this)
}
}
+
+ open fun onInvalidate() {
+ if (parent is View) (parent as View).onInvalidate()
+ }
}
diff --git a/ui/ui-desktop/samples/src/jvmMain/kotlin/androidx/ui/desktop/examples/example1/Main.kt b/ui/ui-desktop/samples/src/jvmMain/kotlin/androidx/ui/desktop/examples/example1/Main.kt
index 0d39c91..66577e8 100644
--- a/ui/ui-desktop/samples/src/jvmMain/kotlin/androidx/ui/desktop/examples/example1/Main.kt
+++ b/ui/ui-desktop/samples/src/jvmMain/kotlin/androidx/ui/desktop/examples/example1/Main.kt
@@ -46,6 +46,8 @@
import androidx.ui.text.font.fontFamily
import androidx.ui.desktop.font
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Row
import androidx.ui.unit.IntSize
private const val title = "Desktop Compose Elements"
@@ -70,6 +72,7 @@
},
bodyContent = {
val amount = state { 0 }
+ val animation = state { true }
val text = state { "Hello" }
Column(Modifier.fillMaxSize(), Arrangement.SpaceEvenly) {
Text(
@@ -126,7 +129,21 @@
}) {
Text("Base")
}
- CircularProgressIndicator()
+
+ Row(modifier = Modifier.padding(vertical = 10.dp),
+ verticalGravity = Alignment.CenterVertically) {
+ Button(
+ >
+ animation.value = !animation.value
+ }) {
+ Text("Toggle")
+ }
+
+ if (animation.value) {
+ CircularProgressIndicator()
+ }
+ }
+
Slider(value = amount.value.toFloat() / 100f,
amount.value = (it * 100).toInt() })
FilledTextField(
diff --git a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/BaseAnimationClock.kt b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/BaseAnimationClock.kt
new file mode 100644
index 0000000..395044b
--- /dev/null
+++ b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/BaseAnimationClock.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.ui.desktop
+
+import androidx.animation.AnimationClockObservable
+import androidx.animation.AnimationClockObserver
+
+/**
+ * Copy-pasted androidx.animation.BaseAnimationClock to make possible to use internal
+ * dispatchTime.
+ * TODO: Move DesktopAnimationClock to ui-animation-core when MPP is ready. Remove this base
+ * class after that
+ */
+abstract class BaseAnimationClock : AnimationClockObservable {
+ // Using LinkedHashSet to increase removal performance
+ private val observers: MutableSet<AnimationClockObserver> = LinkedHashSet()
+
+ private val pendingActions: MutableList<Int> = mutableListOf()
+ private val pendingObservers: MutableList<AnimationClockObserver> = mutableListOf()
+
+ private fun addToPendingActions(action: Int, observer: AnimationClockObserver) =
+ synchronized(pendingActions) {
+ pendingActions.add(action) && pendingObservers.add(observer)
+ }
+
+ private fun pendingActionsIsNotEmpty(): Boolean =
+ synchronized(pendingActions) {
+ pendingActions.isNotEmpty()
+ }
+
+ private inline fun forEachObserver(crossinline action: (AnimationClockObserver) -> Unit) =
+ synchronized(observers) {
+ observers.forEach(action)
+ }
+
+ /**
+ * Subscribes [observer] to this clock. Duplicate subscriptions will be ignored.
+ */
+ override fun subscribe(observer: AnimationClockObserver) {
+ addToPendingActions(AddAction, observer)
+ }
+
+ override fun unsubscribe(observer: AnimationClockObserver) {
+ addToPendingActions(RemoveAction, observer)
+ }
+
+ internal open fun dispatchTime(frameTimeMillis: Long) {
+ processPendingActions()
+
+ forEachObserver {
+ it.onAnimationFrame(frameTimeMillis)
+ }
+
+ while (pendingActionsIsNotEmpty()) {
+ processPendingActions().forEach {
+ it.onAnimationFrame(frameTimeMillis)
+ }
+ }
+ }
+
+ internal fun hasObservers(): Boolean {
+ synchronized(observers) {
+ // Start with processing pending actions: it might remove the last observers
+ processPendingActions()
+ return observers.isNotEmpty()
+ }
+ }
+
+ private fun processPendingActions(): Set<AnimationClockObserver> {
+ synchronized(observers) {
+ synchronized(pendingActions) {
+ if (pendingActions.isEmpty()) {
+ return emptySet()
+ }
+ val additions = LinkedHashSet<AnimationClockObserver>()
+ pendingActions.forEachIndexed { i, action ->
+ when (action) {
+ AddAction -> {
+ // This check ensures that we only have one instance of the observer in
+ // the callbacks at any given time.
+ if (observers.add(pendingObservers[i])) {
+ additions.add(pendingObservers[i])
+ }
+ }
+ RemoveAction -> {
+ observers.remove(pendingObservers[i])
+ additions.remove(pendingObservers[i])
+ }
+ }
+ }
+ pendingActions.clear()
+ pendingObservers.clear()
+ return additions
+ }
+ }
+ }
+
+ private companion object {
+ private const val AddAction = 1
+ private const val RemoveAction = 2
+ }
+}
\ No newline at end of file
diff --git a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/ComposeInit.kt b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/ComposeInit.kt
index 6682a79..590ab06 100644
--- a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/ComposeInit.kt
+++ b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/ComposeInit.kt
@@ -18,7 +18,9 @@
package androidx.ui.desktop
+import androidx.animation.rootAnimationClockFactory
import androidx.compose.InternalComposeApi
+import androidx.compose.dispatch.DesktopUiDispatcher
import androidx.ui.graphics.DesktopCanvas
import androidx.ui.graphics.DesktopImageShader
import androidx.ui.graphics.DesktopInternalCanvasHolder
@@ -69,5 +71,11 @@
GraphicsFactory.Shader.image = ::DesktopImageShader
paragraphIntrinsicsActualFactory = ::DesktopParagraphIntrinsics
paragraphActualFactory = ::DesktopParagraph
+ @OptIn(androidx.animation.InternalAnimationApi::class)
+ rootAnimationClockFactory = {
+ // TODO: detect actual display refresh rate? what to do with displays with
+ // different refresh rates?
+ DesktopAnimationClock(60, DesktopUiDispatcher.Dispatcher)
+ }
}
}
\ No newline at end of file
diff --git a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/DesktopParagraph.kt b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/DesktopParagraph.kt
index 2bb81f1..16b1710 100644
--- a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/DesktopParagraph.kt
+++ b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/DesktopParagraph.kt
@@ -20,13 +20,16 @@
import androidx.ui.graphics.Canvas
import androidx.ui.graphics.DesktopPath
import androidx.ui.graphics.Path
+import androidx.ui.graphics.toAndroidX
import androidx.ui.text.Paragraph
import androidx.ui.text.ParagraphConstraints
import androidx.ui.text.ParagraphIntrinsics
import androidx.ui.text.TextRange
import androidx.ui.text.style.ResolvedTextDirection
+import org.jetbrains.skija.paragraph.LineMetrics
import org.jetbrains.skija.paragraph.RectHeightMode
import org.jetbrains.skija.paragraph.RectWidthMode
+import kotlin.math.floor
internal class DesktopParagraph(
intrinsics: ParagraphIntrinsics,
@@ -56,10 +59,10 @@
get() = paragraphIntrinsics.maxIntrinsicWidth
override val firstBaseline: Float
- get() = para.getLineMetrics().first().baseline.toFloat()
+ get() = para.getLineMetrics().firstOrNull()?.run { baseline.toFloat() } ?: 0f
override val lastBaseline: Float
- get() = para.getLineMetrics().last().baseline.toFloat()
+ get() = para.getLineMetrics().lastOrNull()?.run { baseline.toFloat() } ?: 0f
override val didExceedMaxLines: Boolean
// TODO: support text ellipsize.
@@ -68,10 +71,11 @@
override val lineCount: Int
get() = para.lineNumber.toInt()
- override val placeholderRects: List<Rect?> get() {
- println("Paragraph.placeholderRects")
- return listOf()
- }
+ override val placeholderRects: List<Rect?>
+ get() {
+ println("Paragraph.placeholderRects")
+ return listOf()
+ }
override fun getPathForRange(start: Int, end: Int): Path {
val boxes = para.getRectsForRange(
@@ -88,8 +92,16 @@
}
override fun getCursorRect(offset: Int): Rect {
- println("Paragraph.getCursorRect $offset")
- return Rect(0.0f, 0.0f, 0.0f, 0.0f)
+ val cursorWidth = 4.0f
+ val horizontal = getHorizontalPosition(offset, true)
+ val line = getLineForOffset(offset)
+
+ return Rect(
+ horizontal - 0.5f * cursorWidth,
+ getLineTop(line),
+ horizontal + 0.5f * cursorWidth,
+ getLineBottom(line)
+ )
}
override fun getLineLeft(lineIndex: Int): Float {
@@ -102,35 +114,33 @@
return 0.0f
}
- override fun getLineTop(lineIndex: Int): Float {
- println("Paragraph.getLineTop $lineIndex")
- return 0.0f
+ override fun getLineTop(lineIndex: Int) =
+ para.lineMetrics.getOrNull(lineIndex)?.let { line ->
+ floor((line.baseline - line.ascent).toFloat())
+ } ?: 0f
+
+ override fun getLineBottom(lineIndex: Int) =
+ para.lineMetrics.getOrNull(lineIndex)?.let { line ->
+ floor((line.baseline + line.descent).toFloat())
+ } ?: 0f
+
+ private fun lineMetricsForOffset(offset: Int): LineMetrics? {
+ val metrics = para.lineMetrics
+ for (line in metrics) {
+ if (offset <= line.endIndex) {
+ return line
+ }
+ }
+ return null
}
- override fun getLineBottom(lineIndex: Int): Float {
- println("Paragraph.getLineBottom $lineIndex")
- return 0.0f
- }
+ override fun getLineHeight(lineIndex: Int) = para.lineMetrics[lineIndex].height.toFloat()
- override fun getLineHeight(lineIndex: Int): Float {
- println("Paragraph.getLineHeight $lineIndex")
- return 0.0f
- }
+ override fun getLineWidth(lineIndex: Int) = para.lineMetrics[lineIndex].width.toFloat()
- override fun getLineWidth(lineIndex: Int): Float {
- println("Paragraph.getLineWidth $lineIndex")
- return 0.0f
- }
+ override fun getLineStart(lineIndex: Int) = para.lineMetrics[lineIndex].startIndex.toInt()
- override fun getLineStart(lineIndex: Int): Int {
- println("Paragraph.getLineStart $lineIndex")
- return 0
- }
-
- override fun getLineEnd(lineIndex: Int): Int {
- println("Paragraph.getLineEnd $lineIndex")
- return 0
- }
+ override fun getLineEnd(lineIndex: Int) = para.lineMetrics[lineIndex].endIndex.toInt()
override fun getLineEllipsisOffset(lineIndex: Int): Int {
println("Paragraph.getLineEllipsisOffset $lineIndex")
@@ -142,14 +152,27 @@
return 0
}
- override fun getLineForOffset(offset: Int): Int {
- println("Paragraph.getLineForOffset $offset")
- return 0
- }
+ override fun getLineForOffset(offset: Int) =
+ lineMetricsForOffset(offset)?.run { lineNumber.toInt() }
+ ?: 0
override fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float {
- println("getHorizontalPosition $offset, $usePrimaryDirection")
- return 0.0f
+ val metrics = lineMetricsForOffset(offset)
+
+ return when {
+ metrics == null -> 0f
+ metrics.startIndex.toInt() == offset || metrics.startIndex == metrics.endIndex -> 0f
+ metrics.endIndex.toInt() == offset -> {
+ para.getRectsForRange(offset - 1, offset, RectHeightMode.MAX, RectWidthMode.MAX)
+ .first()
+ .rect.right
+ }
+ else -> {
+ para.getRectsForRange(
+ offset, offset + 1, RectHeightMode.MAX, RectWidthMode.MAX
+ ).first().rect.left
+ }
+ }
}
override fun getParagraphDirection(offset: Int): ResolvedTextDirection =
@@ -162,17 +185,18 @@
return para.getGlyphPositionAtCoordinate(position.x, position.y).position
}
- override fun getBoundingBox(offset: Int): Rect {
- println("getBoundingBox $offset")
- return Rect(0.0f, 0.0f, 0.0f, 0.0f)
- }
+ override fun getBoundingBox(offset: Int) =
+ para.getRectsForRange(
+ offset, offset + 1, RectHeightMode.MAX, RectWidthMode
+ .MAX
+ ).first().rect.toAndroidX()
override fun getWordBoundary(offset: Int): TextRange {
- println("getWordBoundary $offset")
+ println("Paragraph.getWordBoundary $offset")
return TextRange(0, 0)
}
override fun paint(canvas: Canvas) {
para.paint(canvas.nativeCanvas.skijaCanvas, 0.0f, 0.0f)
}
-}
+}
\ No newline at end of file
diff --git a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/SkiaWindow.kt b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/SkiaWindow.kt
index 8a795e2..d846a37 100644
--- a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/SkiaWindow.kt
+++ b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/SkiaWindow.kt
@@ -15,6 +15,8 @@
*/
package androidx.ui.desktop
+import androidx.animation.AnimationClockObserver
+import androidx.compose.dispatch.DesktopUiDispatcher
import androidx.ui.text.platform.paragraphActualFactory
import androidx.ui.text.platform.paragraphIntrinsicsActualFactory
import com.jogamp.opengl.GL
@@ -23,7 +25,6 @@
import com.jogamp.opengl.GLEventListener
import com.jogamp.opengl.GLProfile
import com.jogamp.opengl.awt.GLCanvas
-import com.jogamp.opengl.util.FPSAnimator
import org.jetbrains.skija.BackendRenderTarget
import org.jetbrains.skija.Canvas
import org.jetbrains.skija.ColorSpace
@@ -85,9 +86,8 @@
override val parent: AppFrame
override val glCanvas: GLCanvas
- override var animator: FPSAnimator? = null
override var renderer: SkiaRenderer? = null
- override val vsync = false
+ override val vsync = true
constructor(width: Int, height: Int, parent: AppFrame) : super() {
this.parent = parent
@@ -120,9 +120,8 @@
override val parent: AppFrame
override val glCanvas: GLCanvas
- override var animator: FPSAnimator? = null
override var renderer: SkiaRenderer? = null
- override val vsync = false
+ override val vsync = true
constructor(
attached: JFrame?,
@@ -145,22 +144,9 @@
internal interface SkiaFrame {
val parent: AppFrame
val glCanvas: GLCanvas
- var animator: FPSAnimator?
var renderer: SkiaRenderer?
val vsync: Boolean
- fun setFps(fps: Int) {
- animator?.stop()
- animator = if (fps > 0) {
- FPSAnimator(fps).also {
- it.add(glCanvas)
- it.start()
- }
- } else {
- null
- }
- }
-
fun close() {
glCanvas.destroy()
}
@@ -206,6 +192,21 @@
}
}
+// Simple FPS tracker for debug purposes
+internal class FPSTracker {
+ private var t0 = 0L
+ private val times = DoubleArray(155)
+ private var timesIdx = 0
+
+ fun track() {
+ val t1 = System.nanoTime()
+ times[timesIdx] = (t1 - t0) / 1000000.0
+ t0 = t1
+ timesIdx = (timesIdx + 1) % times.size
+ println("FPS: ${1000 / times.takeWhile { it > 0 }.average()}")
+ }
+}
+
private fun initCanvas(frame: SkiaFrame, vsync: Boolean = false): GLCanvas {
val profile = GLProfile.get(GLProfile.GL3)
val capabilities = GLCapabilities(profile)
@@ -269,10 +270,9 @@
AppManager.removeWindow(frame.parent)
}
- override fun display(drawable: GLAutoDrawable?) {
+ override fun display(drawable: GLAutoDrawable) {
skijaState.apply {
- val gl = drawable!!.gl!!
- // drawable.swapBuffers()
+ val gl = drawable.gl!!
canvas!!.clear(0xFFFFFFF)
gl.glBindTexture(GL.GL_TEXTURE_2D, textureId)
frame.renderer!!.onRender(
@@ -287,4 +287,37 @@
})
return glCanvas
-}
\ No newline at end of file
+}
+
+internal class DesktopAnimationClock(fps: Int, val dispatcher: DesktopUiDispatcher) :
+ BaseAnimationClock() {
+ val delay = 1_000 / fps
+
+ @Volatile
+ private var scheduled = false
+ private fun frameCallback(time: Long) {
+ scheduled = false
+ dispatchTime(time / 1000000)
+ }
+
+ override fun subscribe(observer: AnimationClockObserver) {
+ super.subscribe(observer)
+ scheduleIfNeeded()
+ }
+
+ override fun dispatchTime(frameTimeMillis: Long) {
+ super.dispatchTime(frameTimeMillis)
+ scheduleIfNeeded()
+ }
+
+ private fun scheduleIfNeeded() {
+ when {
+ scheduled -> return
+ !hasObservers() -> return
+ else -> {
+ scheduled = true
+ dispatcher.scheduleCallbackWithDelay(delay, ::frameCallback)
+ }
+ }
+ }
+}
diff --git a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/Wrapper.kt b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/Wrapper.kt
index 307aee3..27c225d 100644
--- a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/Wrapper.kt
+++ b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/desktop/Wrapper.kt
@@ -17,66 +17,35 @@
import android.content.Context
import android.view.MotionEvent
-import androidx.animation.ManualAnimationClock
-import androidx.animation.rootAnimationClockFactory
import androidx.compose.Composable
import androidx.ui.desktop.view.LayoutScope
import javax.swing.SwingUtilities
import org.jetbrains.skija.Canvas
-@OptIn(androidx.animation.InternalAnimationApi::class)
fun Window.setContent(content: @Composable () -> Unit) {
SwingUtilities.invokeLater {
- val fps = 60
- val clocks = mutableListOf<ManualAnimationClock>()
- rootAnimationClockFactory = {
- ManualAnimationClock(0L).also {
- clocks.add(it)
- }
- }
-
- val mainLayout = LayoutScope()
+ val mainLayout = LayoutScope(glCanvas)
mainLayout.setContent(content)
-
this.renderer = Renderer(
mainLayout.context,
- mainLayout.platformInputService,
- clocks,
- fps)
-
- this.setFps(fps)
+ mainLayout.platformInputService)
}
}
-@OptIn(androidx.animation.InternalAnimationApi::class)
fun Dialog.setContent(content: @Composable () -> Unit) {
SwingUtilities.invokeLater {
- val fps = 60
- val clocks = mutableListOf<ManualAnimationClock>()
- rootAnimationClockFactory = {
- ManualAnimationClock(0L).also {
- clocks.add(it)
- }
- }
-
- val mainLayout = LayoutScope()
+ val mainLayout = LayoutScope(glCanvas)
mainLayout.setContent(content)
this.renderer = Renderer(
mainLayout.context,
- mainLayout.platformInputService,
- clocks,
- fps)
-
- this.setFps(fps)
+ mainLayout.platformInputService)
}
}
private class Renderer(
val context: Context,
- val platformInputService: DesktopPlatformInput,
- val clocks: List<ManualAnimationClock>,
- val fps: Int
+ val platformInputService: DesktopPlatformInput
) : SkiaRenderer {
private val canvases = mutableMapOf<LayoutScope, Canvas?>()
@@ -107,13 +76,9 @@
override fun onReshape(canvas: Canvas, width: Int, height: Int) {
clearCanvases()
- draw(canvas, width, height)
}
override fun onRender(canvas: Canvas, width: Int, height: Int) {
- clocks.forEach {
- it.clockTimeMillis += 1000 / fps
- }
draw(canvas, width, height)
}
diff --git a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/graphics/Rects.kt b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/graphics/Rects.kt
index 461cc99..4e7bbe8 100644
--- a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/graphics/Rects.kt
+++ b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/graphics/Rects.kt
@@ -24,4 +24,7 @@
right,
bottom
)
-}
\ No newline at end of file
+}
+
+internal fun org.jetbrains.skija.Rect.toAndroidX() =
+ Rect(left, top, right, bottom)
\ No newline at end of file
diff --git a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/view/LayoutScope.kt b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/view/LayoutScope.kt
index 31a8c1b..39ff31e 100644
--- a/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/view/LayoutScope.kt
+++ b/ui/ui-desktop/src/jvmMain/kotlin/androidx/ui/view/LayoutScope.kt
@@ -22,6 +22,7 @@
import androidx.compose.Composable
import androidx.compose.Providers
import androidx.compose.Recomposer
+import androidx.compose.dispatch.DesktopUiDispatcher
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
@@ -38,6 +39,7 @@
import androidx.ui.desktop.LayoutScopeGlobal
import androidx.ui.input.TextInputService
import androidx.ui.unit.IntOffset
+import com.jogamp.opengl.awt.GLCanvas
import org.jetbrains.skija.Canvas
class LayoutScope {
@@ -52,9 +54,26 @@
val width: Int get() = layout.right - layout.left
val height: Int get() = layout.bottom - layout.top
- constructor() {
+ // Optimization: we don't need more than one redrawing per tick
+ var redrawingScheduled = false
+
+ constructor(glCanvas: GLCanvas) {
context = object : Context() {}
- layout = object : ViewGroup(context) {}
+ layout = object : ViewGroup(context) {
+ override fun onInvalidate() {
+ if (!redrawingScheduled) {
+ DesktopUiDispatcher.Dispatcher.scheduleAfterCallback {
+ redrawingScheduled = false
+ if (Recomposer.current().hasPendingChanges()) {
+ onInvalidate()
+ } else {
+ redraw(glCanvas)
+ }
+ }
+ redrawingScheduled = true
+ }
+ }
+ }
}
constructor(composeView: View, context: Context) {
@@ -66,6 +85,10 @@
internal lateinit var platformInputService: DesktopPlatformInput
private set
+ private fun redraw(glCanvas: GLCanvas) {
+ glCanvas.display()
+ }
+
fun setContent(content: @Composable () -> Unit) {
platformInputService = DesktopPlatformInput()
ViewTreeLifecycleOwner.set(layout, object : LifecycleOwner {