From 6dddac94f5f5e7d01a6e4734efec8ed852e67553 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Thu, 27 Apr 2023 14:43:47 -0400 Subject: [PATCH] Hook up CADisplayLink for a frame pulse on iOS --- .../redwood/treehouse/DisplayLinkTarget.kt | 53 +++++++++++++++++++ .../redwood/treehouse/IosDisplayLinkClock.kt | 45 ++++++++++++++++ .../treehouse/treehouseAppFactoryIos.kt | 2 +- 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/DisplayLinkTarget.kt create mode 100644 redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/IosDisplayLinkClock.kt diff --git a/redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/DisplayLinkTarget.kt b/redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/DisplayLinkTarget.kt new file mode 100644 index 0000000000..0edf40ad69 --- /dev/null +++ b/redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/DisplayLinkTarget.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 app.cash.redwood.treehouse + +import kotlinx.cinterop.ExportObjCClass +import kotlinx.cinterop.ObjCAction +import platform.Foundation.NSRunLoop +import platform.Foundation.NSSelectorFromString +import platform.QuartzCore.CADisplayLink +import platform.darwin.NSObject + +@ExportObjCClass +internal class DisplayLinkTarget( + private val callback: DisplayLinkTarget.() -> Unit, +) : NSObject() { + private val displayLink: CADisplayLink = CADisplayLink.displayLinkWithTarget( + target = this, + selector = NSSelectorFromString(this::onFrame.name), + ) + + /** This function must be public to be a valid candidate for [NSSelectorString]. */ + @ObjCAction + public fun onFrame() { + callback(this) + } + + fun subscribe() { + displayLink.addToRunLoop( + NSRunLoop.currentRunLoop, + NSRunLoop.currentRunLoop.currentMode, + ) + } + + fun unsubscribe() { + displayLink.removeFromRunLoop( + NSRunLoop.currentRunLoop, + NSRunLoop.currentRunLoop.currentMode, + ) + } +} diff --git a/redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/IosDisplayLinkClock.kt b/redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/IosDisplayLinkClock.kt new file mode 100644 index 0000000000..6e64de212c --- /dev/null +++ b/redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/IosDisplayLinkClock.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 app.cash.redwood.treehouse + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +public class IosDisplayLinkClock : FrameClock { + private lateinit var scope: CoroutineScope + private lateinit var dispatchers: TreehouseDispatchers + private lateinit var displayLinkTarget: DisplayLinkTarget + + /** Non-null if we're expecting a call to [tickClock]. */ + private var appLifecycle: AppLifecycle? = null + + override fun start(scope: CoroutineScope, dispatchers: TreehouseDispatchers) { + this.scope = scope + this.dispatchers = dispatchers + this.displayLinkTarget = DisplayLinkTarget { + unsubscribe() + scope.launch(dispatchers.zipline) { + appLifecycle?.sendFrame(0L) + appLifecycle = null + } + } + } + + override fun requestFrame(appLifecycle: AppLifecycle) { + this.appLifecycle = appLifecycle + this.displayLinkTarget.subscribe() + } +} diff --git a/redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/treehouseAppFactoryIos.kt b/redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/treehouseAppFactoryIos.kt index d10c36246a..608bfe423e 100644 --- a/redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/treehouseAppFactoryIos.kt +++ b/redwood-treehouse-host/src/iosMain/kotlin/app/cash/redwood/treehouse/treehouseAppFactoryIos.kt @@ -41,7 +41,7 @@ public fun TreehouseAppFactory( dispatchers = IosTreehouseDispatchers(), eventListener = eventListener, httpClient = httpClient, - frameClock = FixedDelayFrameClock(), + frameClock = IosDisplayLinkClock(), manifestVerifier = manifestVerifier, embeddedDir = embeddedDir, embeddedFileSystem = embeddedFileSystem,