Unverified Commit 163aec11 authored by Elijah Semyonov's avatar Elijah Semyonov Committed by GitHub

Refactoring pass for iOS counterpart of Skiko (#781)

* Refactor SkikoUIView and Metal interaction

* Move load logic to init

* Remove dead code

* Use contentScaleFactor for calculating drawable size

* Move UITouch.isPressed to filescope

* Add trace logs for anomaly situations
parent ce227e6b
...@@ -3,45 +3,30 @@ package org.jetbrains.skiko ...@@ -3,45 +3,30 @@ package org.jetbrains.skiko
import kotlinx.cinterop.useContents import kotlinx.cinterop.useContents
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
import org.jetbrains.skia.PixelGeometry import org.jetbrains.skia.PixelGeometry
import org.jetbrains.skiko.context.MetalContextHandler import org.jetbrains.skia.Surface
import org.jetbrains.skiko.redrawer.MetalRedrawer
import platform.UIKit.* import platform.UIKit.*
import kotlin.system.getTimeNanos import kotlin.system.getTimeNanos
import org.jetbrains.skia.*
actual open class SkiaLayer { actual open class SkiaLayer {
internal var needRedrawCallback: () -> Unit = {}
fun isShowing(): Boolean {
return true
}
fun showScreenKeyboard() {
view?.becomeFirstResponder()
}
fun hideScreenKeyboard() { view?.resignFirstResponder() }
fun isScreenKeyboardOpen(): Boolean {
return if (view == null) false else view!!.isFirstResponder
}
actual var renderApi: GraphicsApi actual var renderApi: GraphicsApi
get() = GraphicsApi.METAL get() = GraphicsApi.METAL
set(value) { throw UnsupportedOperationException() } set(_) { throw UnsupportedOperationException() }
actual val contentScale: Float actual val contentScale: Float
get() = view!!.contentScaleFactor.toFloat() get() = view!!.contentScaleFactor.toFloat()
actual var fullscreen: Boolean actual var fullscreen: Boolean
get() = true get() = true
set(value) { throw UnsupportedOperationException() } set(_) { throw UnsupportedOperationException() }
actual var transparency: Boolean actual var transparency: Boolean
get() = false get() = false
set(value) { throw UnsupportedOperationException() } set(_) { throw UnsupportedOperationException() }
actual fun needRedraw() { actual fun needRedraw() {
redrawer?.needRedraw() needRedrawCallback.invoke()
} }
actual val component: Any? actual val component: Any?
...@@ -57,56 +42,24 @@ actual open class SkiaLayer { ...@@ -57,56 +42,24 @@ actual open class SkiaLayer {
return@useContents size.height.toFloat() return@useContents size.height.toFloat()
} }
internal var view: UIView? = null internal var view: SkikoUIView? = null
// We need to keep reference to gesturesDetector as Objective-C will only keep weak reference here.
internal var gesturesDetector = SkikoGesturesDetector(this)
var gesturesToListen: Array<SkikoGestureEventKind>? = null
set(value) {
field = value
initGestures()
}
internal fun initGestures() {
gesturesDetector.setGesturesToListen(gesturesToListen)
}
actual fun attachTo(container: Any) { actual fun attachTo(container: Any) {
attachTo(container as UIView) view = container as SkikoUIView
} }
fun attachTo(view: UIView) {
this.view = view
contextHandler = MetalContextHandler(this)
// TODO: maybe add observer for view.viewDidDisappear() to detach us?
redrawer = MetalRedrawer(this).apply {
needRedraw()
}
}
private var isDisposed = false
actual fun detach() { actual fun detach() {
if (!isDisposed) { view?.detach()
redrawer?.dispose()
redrawer = null
contextHandler?.dispose()
contextHandler = null
isDisposed = true
}
} }
actual var skikoView: SkikoView? = null
internal var redrawer: MetalRedrawer? = null actual var skikoView: SkikoView? = null
private var contextHandler: MetalContextHandler? = null
internal actual fun draw(canvas: Canvas) { internal actual fun draw(canvas: Canvas) {
check(!isDisposed) { "SkiaLayer is disposed" } throw UnsupportedOperationException("Don't call it, artifact of wrong abstraction")
val (w, h) = view!!.frame.useContents { }
size.width to size.height
}
val pictureWidth = (w.toFloat() * contentScale).coerceAtLeast(0.0F)
val pictureHeight = (h.toFloat() * contentScale).coerceAtLeast(0.0F)
skikoView?.onRender(canvas, pictureWidth.toInt(), pictureHeight.toInt(), getTimeNanos()) internal fun draw(surface: Surface) {
skikoView?.onRender(surface.canvas, surface.width, surface.height, getTimeNanos())
} }
actual val pixelGeometry: PixelGeometry actual val pixelGeometry: PixelGeometry
......
package org.jetbrains.skiko
import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.useContents
import platform.darwin.NSObject
import platform.Foundation.NSSelectorFromString
import platform.UIKit.*
// See https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events?language=objc
internal class SkikoGesturesDetector(
private val layer: SkiaLayer
) : NSObject() {
val view: UIView?
get() = layer.view
@ObjCAction
fun onTap(sender: UITapGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.TAP,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onDoubleTap(sender: UITapGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.DOUBLETAP,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onLongPress(sender: UILongPressGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.LONGPRESS,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onPinch(sender: UIPinchGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.PINCH,
scale = sender.scale,
velocity = sender.velocity,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onRotation(sender: UIRotationGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.ROTATION,
rotation = sender.rotation,
velocity = sender.velocity,
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onSwipe(sender: UISwipeGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.SWIPE,
direction = toSkikoGestureDirection(sender.direction),
state = toSkikoGestureState(sender.state)
)
)
}
@ObjCAction
fun onPan(sender: UIPanGestureRecognizer) {
val (x, y) = sender.locationInView(view).useContents { x to y }
layer.skikoView?.onGestureEvent(
SkikoGestureEvent(
x = x,
y = y,
kind = SkikoGestureEventKind.PAN,
state = toSkikoGestureState(sender.state)
)
)
}
// We have ':' in selector to take care of function argument.
private val gestureRecognizers = hashMapOf(
SkikoGestureEventKind.TAP to UITapGestureRecognizer(this, NSSelectorFromString("onTap:")),
SkikoGestureEventKind.DOUBLETAP to UITapGestureRecognizer(this, NSSelectorFromString("onDoubleTap:")).apply {
numberOfTapsRequired = 2.toULong()
},
SkikoGestureEventKind.LONGPRESS to UILongPressGestureRecognizer(this, NSSelectorFromString("onLongPress:")),
SkikoGestureEventKind.PINCH to UIPinchGestureRecognizer(this, NSSelectorFromString("onPinch:")),
SkikoGestureEventKind.ROTATION to UIRotationGestureRecognizer(this, NSSelectorFromString("onRotation:")),
SkikoGestureEventKind.SWIPE to UISwipeGestureRecognizer(this, NSSelectorFromString("onSwipe:")),
SkikoGestureEventKind.PAN to UIPanGestureRecognizer(this, NSSelectorFromString("onPan:"))
)
fun setGesturesToListen(gestures: Array<SkikoGestureEventKind>?) {
clearGesturesToListen()
if (!gestures.isNullOrEmpty()) {
for (gesture in gestures) {
if (gestureRecognizers.containsKey(gesture)) {
view?.addGestureRecognizer(gestureRecognizers.get(gesture)!!)
}
}
}
}
private fun clearGesturesToListen() {
for ((key, value) in gestureRecognizers) {
view?.removeGestureRecognizer(value)
}
}
}
...@@ -4,8 +4,13 @@ import kotlinx.cinterop.* ...@@ -4,8 +4,13 @@ import kotlinx.cinterop.*
import org.jetbrains.skia.Point import org.jetbrains.skia.Point
import org.jetbrains.skia.Rect import org.jetbrains.skia.Rect
import org.jetbrains.skiko.ios.SkikoUITextInputTraits import org.jetbrains.skiko.ios.SkikoUITextInputTraits
import org.jetbrains.skiko.redrawer.MetalRedrawer
import platform.CoreGraphics.* import platform.CoreGraphics.*
import platform.Foundation.* import platform.Foundation.*
import platform.Metal.MTLCreateSystemDefaultDevice
import platform.Metal.MTLDeviceProtocol
import platform.Metal.MTLPixelFormatBGRA8Unorm
import platform.QuartzCore.CAMetalLayer
import platform.UIKit.* import platform.UIKit.*
import platform.darwin.NSInteger import platform.darwin.NSInteger
import kotlin.math.max import kotlin.math.max
...@@ -18,40 +23,83 @@ import kotlin.math.min ...@@ -18,40 +23,83 @@ import kotlin.math.min
@Suppress("CONFLICTING_OVERLOADS") @Suppress("CONFLICTING_OVERLOADS")
@ExportObjCClass @ExportObjCClass
class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
@OverrideInit companion object : UIViewMeta() {
constructor(frame: CValue<CGRect>) : super(frame) override fun layerClass() = CAMetalLayer
}
@Suppress("UNUSED") // required by Objective-C runtime
@OverrideInit @OverrideInit
constructor(coder: NSCoder) : super(coder) constructor(coder: NSCoder) : super(coder) {
throw UnsupportedOperationException("init(coder: NSCoder) is not supported for SkikoUIView")
init {
multipleTouchEnabled = true
} }
private var skiaLayer: SkiaLayer? = null private val _device: MTLDeviceProtocol =
MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Metal is not supported on this system")
private val _metalLayer: CAMetalLayer get() = layer as CAMetalLayer
private var _skiaLayer: SkiaLayer? = null
private var _pointInside: (Point, UIEvent?) -> Boolean = { _, _ -> true } private var _pointInside: (Point, UIEvent?) -> Boolean = { _, _ -> true }
private var _skikoUITextInputTrains: SkikoUITextInputTraits = object : SkikoUITextInputTraits {} private var _skikoUITextInputTrains: SkikoUITextInputTraits = object : SkikoUITextInputTraits {}
private var _inputDelegate: UITextInputDelegateProtocol? = null private var _inputDelegate: UITextInputDelegateProtocol? = null
private var _currentTextMenuActions: TextActions? = null private var _currentTextMenuActions: TextActions? = null
private lateinit var _redrawer: MetalRedrawer
/*
* When there at least one tracked touch, we need notify redrawer about it. It should schedule CADisplayLink which
* affects frequency of polling UITouch events on high frequency display and forces it to match display refresh rate.
*/
private var _touchesCount = 0
set(value) {
field = value
val needHighFrequencyPolling = value > 0
_redrawer.needsProactiveDisplayLink = needHighFrequencyPolling
}
init {
multipleTouchEnabled = true
opaque = false // For UIKit interop through a "Hole"
_metalLayer.also {
// Workaround for KN compiler bug
// Type mismatch: inferred type is platform.Metal.MTLDeviceProtocol but objcnames.protocols.MTLDeviceProtocol? was expected
@Suppress("USELESS_CAST")
it.device = _device as objcnames.protocols.MTLDeviceProtocol?
it.pixelFormat = MTLPixelFormatBGRA8Unorm
doubleArrayOf(0.0, 0.0, 0.0, 0.0).usePinned { pinned ->
it.backgroundColor = CGColorCreate(CGColorSpaceCreateDeviceRGB(), pinned.addressOf(0))
}
it.framebufferOnly = false
}
}
// merging two constructors might cause a binary incompatibility, which will result in a unclear linking error, // merging two constructors might cause a binary incompatibility, which will result in a unclear linking error,
// if a project using newer compose depends on an older compose transitively // if a project using newer compose depends on an older compose transitively
// https://youtrack.jetbrains.com/issue/KT-60399 // https://youtrack.jetbrains.com/issue/KT-60399
@Suppress("UNUSED") // public API
constructor( constructor(
skiaLayer: SkiaLayer, skiaLayer: SkiaLayer,
frame: CValue<CGRect> = CGRectNull.readValue(), frame: CValue<CGRect> = CGRectNull.readValue(),
pointInside: (Point, UIEvent?) -> Boolean = {_,_-> true } pointInside: (Point, UIEvent?) -> Boolean = { _, _ -> true }
) : this(skiaLayer, frame, pointInside, skikoUITextInputTrains = object : SkikoUITextInputTraits {}) ) : this(skiaLayer, frame, pointInside, skikoUITextInputTrains = object : SkikoUITextInputTraits {})
constructor( constructor(
skiaLayer: SkiaLayer, skiaLayer: SkiaLayer,
frame: CValue<CGRect> = CGRectNull.readValue(), frame: CValue<CGRect> = CGRectNull.readValue(),
pointInside: (Point, UIEvent?) -> Boolean = {_,_-> true }, pointInside: (Point, UIEvent?) -> Boolean = { _, _ -> true },
skikoUITextInputTrains: SkikoUITextInputTraits skikoUITextInputTrains: SkikoUITextInputTraits
) : super(frame) { ) : super(frame) {
this.skiaLayer = skiaLayer _skiaLayer = skiaLayer
_pointInside = pointInside _pointInside = pointInside
_skikoUITextInputTrains = skikoUITextInputTrains _skikoUITextInputTrains = skikoUITextInputTrains
_redrawer = MetalRedrawer(_metalLayer) { surface ->
skiaLayer.draw(surface)
}
skiaLayer.needRedrawCallback = _redrawer::needRedraw
skiaLayer.view = this
} }
/** /**
...@@ -88,7 +136,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -88,7 +136,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
menu.hideMenu() menu.hideMenu()
} }
fun isTextMenuShown():Boolean { fun isTextMenuShown(): Boolean {
return _currentTextMenuActions != null return _currentTextMenuActions != null
} }
...@@ -108,20 +156,32 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -108,20 +156,32 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
_currentTextMenuActions?.selectAll?.invoke() _currentTextMenuActions?.selectAll?.invoke()
} }
fun detach() = skiaLayer?.detach() internal fun detach() {
_redrawer.dispose()
}
fun load(): SkikoUIView { fun load(): SkikoUIView {
val (width, height) = UIScreen.mainScreen.bounds.useContents { // TODO: redundant, remove in next refactor pass
this.size.width to this.size.height return this
}
override fun didMoveToWindow() {
super.didMoveToWindow()
window?.screen?.let {
contentScaleFactor = it.scale
_redrawer.maximumFramesPerSecond = it.maximumFramesPerSecond
} }
setFrame(CGRectMake(0.0, 0.0, width, height)) }
contentScaleFactor = UIScreen.mainScreen.scale
skiaLayer?.let { layer -> override fun layoutSubviews() {
layer.attachTo(this) super.layoutSubviews()
layer.initGestures()
val scaledSize = bounds.useContents {
CGSizeMake(size.width * contentScaleFactor, size.height * contentScaleFactor)
} }
return this _metalLayer.drawableSize = scaledSize
} }
fun showScreenKeyboard() = becomeFirstResponder() fun showScreenKeyboard() = becomeFirstResponder()
...@@ -133,7 +193,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -133,7 +193,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uikeyinput/1614457-hastext * https://developer.apple.com/documentation/uikit/uikeyinput/1614457-hastext
*/ */
override fun hasText(): Boolean { override fun hasText(): Boolean {
return skiaLayer?.skikoView?.input?.hasText() ?: false return _skiaLayer?.skikoView?.input?.hasText() ?: false
} }
/** /**
...@@ -143,7 +203,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -143,7 +203,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* @param text A string object representing the character typed on the system keyboard. * @param text A string object representing the character typed on the system keyboard.
*/ */
override fun insertText(text: String) { override fun insertText(text: String) {
skiaLayer?.skikoView?.input?.insertText(text) _skiaLayer?.skikoView?.input?.insertText(text)
} }
/** /**
...@@ -152,7 +212,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -152,7 +212,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uikeyinput/1614572-deletebackward * https://developer.apple.com/documentation/uikit/uikeyinput/1614572-deletebackward
*/ */
override fun deleteBackward() { override fun deleteBackward() {
skiaLayer?.skikoView?.input?.deleteBackward() _skiaLayer?.skikoView?.input?.deleteBackward()
} }
override fun canBecomeFirstResponder() = true override fun canBecomeFirstResponder() = true
...@@ -162,7 +222,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -162,7 +222,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
for (press in withEvent.allPresses) { for (press in withEvent.allPresses) {
val uiPress = press as? UIPress val uiPress = press as? UIPress
if (uiPress != null) { if (uiPress != null) {
skiaLayer?.skikoView?.onKeyboardEvent( _skiaLayer?.skikoView?.onKeyboardEvent(
toSkikoKeyboardEvent(press, SkikoKeyboardEventKind.DOWN) toSkikoKeyboardEvent(press, SkikoKeyboardEventKind.DOWN)
) )
} }
...@@ -176,7 +236,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -176,7 +236,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
for (press in withEvent.allPresses) { for (press in withEvent.allPresses) {
val uiPress = press as? UIPress val uiPress = press as? UIPress
if (uiPress != null) { if (uiPress != null) {
skiaLayer?.skikoView?.onKeyboardEvent( _skiaLayer?.skikoView?.onKeyboardEvent(
toSkikoKeyboardEvent(press, SkikoKeyboardEventKind.UP) toSkikoKeyboardEvent(press, SkikoKeyboardEventKind.UP)
) )
} }
...@@ -193,23 +253,10 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -193,23 +253,10 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
return _pointInside(skiaPoint, withEvent) return _pointInside(skiaPoint, withEvent)
} }
/*
* When there at least one tracked touch, we need notify redrawer about it. It should schedule CADisplayLink which
* affects frequency of polling UITouch events on high frequency display and forces it to match display refresh rate.
*/
private var touchesCount = 0
set(value) {
field = value
val needHighFrequencyPolling = value > 0
skiaLayer?.redrawer?.needsProactiveDisplayLink = needHighFrequencyPolling
}
override fun touchesBegan(touches: Set<*>, withEvent: UIEvent?) { override fun touchesBegan(touches: Set<*>, withEvent: UIEvent?) {
super.touchesBegan(touches, withEvent) super.touchesBegan(touches, withEvent)
touchesCount += touches.size _touchesCount += touches.size
sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.DOWN) sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.DOWN)
} }
...@@ -217,7 +264,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -217,7 +264,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
override fun touchesEnded(touches: Set<*>, withEvent: UIEvent?) { override fun touchesEnded(touches: Set<*>, withEvent: UIEvent?) {
super.touchesEnded(touches, withEvent) super.touchesEnded(touches, withEvent)
touchesCount -= touches.size _touchesCount -= touches.size
sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP) sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP)
} }
...@@ -230,7 +277,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -230,7 +277,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
override fun touchesCancelled(touches: Set<*>, withEvent: UIEvent?) { override fun touchesCancelled(touches: Set<*>, withEvent: UIEvent?) {
super.touchesCancelled(touches, withEvent) super.touchesCancelled(touches, withEvent)
touchesCount -= touches.size _touchesCount -= touches.size
sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP) sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP)
} }
...@@ -249,7 +296,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -249,7 +296,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
) )
} }
skiaLayer?.skikoView?.onPointerEvent( _skiaLayer?.skikoView?.onPointerEvent(
SkikoPointerEvent( SkikoPointerEvent(
x = pointers.centroidX, x = pointers.centroidX,
y = pointers.centroidY, y = pointers.centroidY,
...@@ -261,10 +308,6 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -261,10 +308,6 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
) )
} }
private val UITouch.isPressed get() =
phase != UITouchPhase.UITouchPhaseEnded &&
phase != UITouchPhase.UITouchPhaseCancelled
override fun inputDelegate(): UITextInputDelegateProtocol? { override fun inputDelegate(): UITextInputDelegateProtocol? {
return _inputDelegate return _inputDelegate
} }
...@@ -280,7 +323,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -280,7 +323,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* @return A substring of a document that falls within the specified range. * @return A substring of a document that falls within the specified range.
*/ */
override fun textInRange(range: UITextRange): String? { override fun textInRange(range: UITextRange): String? {
return skiaLayer?.skikoView?.input?.textInRange(range.toIntRange()) return _skiaLayer?.skikoView?.input?.textInRange(range.toIntRange())
} }
/** /**
...@@ -290,11 +333,11 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -290,11 +333,11 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* @param withText A string to replace the text in range. * @param withText A string to replace the text in range.
*/ */
override fun replaceRange(range: UITextRange, withText: String) { override fun replaceRange(range: UITextRange, withText: String) {
skiaLayer?.skikoView?.input?.replaceRange(range.toIntRange(), withText) _skiaLayer?.skikoView?.input?.replaceRange(range.toIntRange(), withText)
} }
override fun setSelectedTextRange(selectedTextRange: UITextRange?) { override fun setSelectedTextRange(selectedTextRange: UITextRange?) {
skiaLayer?.skikoView?.input?.setSelectedTextRange(selectedTextRange?.toIntRange()) _skiaLayer?.skikoView?.input?.setSelectedTextRange(selectedTextRange?.toIntRange())
} }
/** /**
...@@ -305,7 +348,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -305,7 +348,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uitextinput/1614541-selectedtextrange * https://developer.apple.com/documentation/uikit/uitextinput/1614541-selectedtextrange
*/ */
override fun selectedTextRange(): UITextRange? { override fun selectedTextRange(): UITextRange? {
return skiaLayer?.skikoView?.input?.getSelectedTextRange()?.toUITextRange() return _skiaLayer?.skikoView?.input?.getSelectedTextRange()?.toUITextRange()
} }
/** /**
...@@ -317,7 +360,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -317,7 +360,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange * https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange
*/ */
override fun markedTextRange(): UITextRange? { override fun markedTextRange(): UITextRange? {
return skiaLayer?.skikoView?.input?.markedTextRange()?.toUITextRange() return _skiaLayer?.skikoView?.input?.markedTextRange()?.toUITextRange()
} }
override fun setMarkedTextStyle(markedTextStyle: Map<Any?, *>?) { override fun setMarkedTextStyle(markedTextStyle: Map<Any?, *>?) {
...@@ -342,7 +385,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -342,7 +385,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
location.toInt() to length.toInt() location.toInt() to length.toInt()
} }
val relativeTextRange = locationRelative until locationRelative + lengthRelative val relativeTextRange = locationRelative until locationRelative + lengthRelative
skiaLayer?.skikoView?.input?.setMarkedText(markedText, relativeTextRange) _skiaLayer?.skikoView?.input?.setMarkedText(markedText, relativeTextRange)
} }
/** /**
...@@ -351,7 +394,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -351,7 +394,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uitextinput/1614512-unmarktext * https://developer.apple.com/documentation/uikit/uitextinput/1614512-unmarktext
*/ */
override fun unmarkText() { override fun unmarkText() {
skiaLayer?.skikoView?.input?.unmarkText() _skiaLayer?.skikoView?.input?.unmarkText()
} }
override fun beginningOfDocument(): UITextPosition { override fun beginningOfDocument(): UITextPosition {
...@@ -363,7 +406,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -363,7 +406,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
* https://developer.apple.com/documentation/uikit/uitextinput/1614555-endofdocument * https://developer.apple.com/documentation/uikit/uitextinput/1614555-endofdocument
*/ */
override fun endOfDocument(): UITextPosition { override fun endOfDocument(): UITextPosition {
return IntermediateTextPosition(skiaLayer?.skikoView?.input?.endOfDocument() ?: 0) return IntermediateTextPosition(_skiaLayer?.skikoView?.input?.endOfDocument() ?: 0)
} }
/** /**
...@@ -383,7 +426,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -383,7 +426,7 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
*/ */
override fun positionFromPosition(position: UITextPosition, offset: NSInteger): UITextPosition? { override fun positionFromPosition(position: UITextPosition, offset: NSInteger): UITextPosition? {
val p = (position as? IntermediateTextPosition)?.position ?: return null val p = (position as? IntermediateTextPosition)?.position ?: return null
val endOfDocument = skiaLayer?.skikoView?.input?.endOfDocument() val endOfDocument = _skiaLayer?.skikoView?.input?.endOfDocument()
return if (endOfDocument != null) { return if (endOfDocument != null) {
IntermediateTextPosition(max(min(p + offset, endOfDocument), 0)) IntermediateTextPosition(max(min(p + offset, endOfDocument), 0))
} else { } else {
...@@ -485,12 +528,16 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -485,12 +528,16 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
when (action) { when (action) {
NSSelectorFromString(UIResponderStandardEditActionsProtocol::copy.name + ":") -> NSSelectorFromString(UIResponderStandardEditActionsProtocol::copy.name + ":") ->
_currentTextMenuActions?.copy != null _currentTextMenuActions?.copy != null
NSSelectorFromString(UIResponderStandardEditActionsProtocol::cut.name + ":") -> NSSelectorFromString(UIResponderStandardEditActionsProtocol::cut.name + ":") ->
_currentTextMenuActions?.cut != null _currentTextMenuActions?.cut != null
NSSelectorFromString(UIResponderStandardEditActionsProtocol::paste.name + ":") -> NSSelectorFromString(UIResponderStandardEditActionsProtocol::paste.name + ":") ->
_currentTextMenuActions?.paste != null _currentTextMenuActions?.paste != null
NSSelectorFromString(UIResponderStandardEditActionsProtocol::selectAll.name + ":") -> NSSelectorFromString(UIResponderStandardEditActionsProtocol::selectAll.name + ":") ->
_currentTextMenuActions?.selectAll != null _currentTextMenuActions?.selectAll != null
else -> false else -> false
} }
...@@ -500,7 +547,9 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol { ...@@ -500,7 +547,9 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
override fun textContentType(): UITextContentType? = _skikoUITextInputTrains.textContentType() override fun textContentType(): UITextContentType? = _skikoUITextInputTrains.textContentType()
override fun isSecureTextEntry(): Boolean = _skikoUITextInputTrains.isSecureTextEntry() override fun isSecureTextEntry(): Boolean = _skikoUITextInputTrains.isSecureTextEntry()
override fun enablesReturnKeyAutomatically(): Boolean = _skikoUITextInputTrains.enablesReturnKeyAutomatically() override fun enablesReturnKeyAutomatically(): Boolean = _skikoUITextInputTrains.enablesReturnKeyAutomatically()
override fun autocapitalizationType(): UITextAutocapitalizationType = _skikoUITextInputTrains.autocapitalizationType() override fun autocapitalizationType(): UITextAutocapitalizationType =
_skikoUITextInputTrains.autocapitalizationType()
override fun autocorrectionType(): UITextAutocorrectionType = _skikoUITextInputTrains.autocorrectionType() override fun autocorrectionType(): UITextAutocorrectionType = _skikoUITextInputTrains.autocorrectionType()
override fun dictationRecognitionFailed() { override fun dictationRecognitionFailed() {
...@@ -574,3 +623,8 @@ private fun NSWritingDirection.directionToStr() = ...@@ -574,3 +623,8 @@ private fun NSWritingDirection.directionToStr() =
UITextLayoutDirectionDown -> "Down" UITextLayoutDirectionDown -> "Down"
else -> "unknown direction" else -> "unknown direction"
} }
private val UITouch.isPressed
get() =
phase != UITouchPhase.UITouchPhaseEnded &&
phase != UITouchPhase.UITouchPhaseCancelled
package org.jetbrains.skiko
import kotlinx.cinterop.*
import platform.Foundation.*
import platform.UIKit.*
@ExportObjCClass
class SkikoViewController : UIViewController {
@OverrideInit
constructor() : super(nibName = null, bundle = null)
@OverrideInit
constructor(coder: NSCoder) : super(coder)
constructor(skikoUIView: SkikoUIView) : this() {
this.skikoUIView = skikoUIView
}
private var skikoUIView: SkikoUIView? = null
override fun loadView() {
if (skikoUIView == null) {
super.loadView()
} else {
this.view = skikoUIView!!.load()
}
}
override fun viewDidLoad() {
super.viewDidLoad()
skikoUIView?.showScreenKeyboard()
}
// viewDidUnload() is deprecated and not called.
override fun viewDidDisappear(animated: Boolean) {
skikoUIView?.detach()
}
}
package org.jetbrains.skiko.context
import kotlinx.cinterop.useContents
import org.jetbrains.skia.*
import org.jetbrains.skiko.RenderException
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.redrawer.MetalRedrawer
internal class MetalContextHandler(layer: SkiaLayer) : ContextHandler(layer, layer::draw) {
val metalRedrawer: MetalRedrawer
get() = layer.redrawer!!
override fun initContext(): Boolean {
try {
if (context == null) {
context = metalRedrawer.makeContext()
}
} catch (e: Exception) {
println("${e.message}\nFailed to create Skia Metal context!")
return false
}
return true
}
private var currentWidth = 0
private var currentHeight = 0
private fun isSizeChanged(width: Int, height: Int): Boolean {
if (width != currentWidth || height != currentHeight) {
currentWidth = width
currentHeight = height
return true
}
return false
}
override fun initCanvas() {
disposeCanvas()
val scale = layer.contentScale
val (w, h) = layer.view!!.frame.useContents {
(size.width * scale).toInt().coerceAtLeast(0) to (size.height * scale).toInt().coerceAtLeast(0)
}
if (isSizeChanged(w, h)) {
metalRedrawer.syncSize()
}
if (w > 0 && h > 0) {
renderTarget = metalRedrawer.makeRenderTarget(w, h)
surface = Surface.makeFromBackendRenderTarget(
context!!,
renderTarget!!,
SurfaceOrigin.TOP_LEFT,
SurfaceColorFormat.BGRA_8888,
ColorSpace.sRGB,
SurfaceProps(pixelGeometry = layer.pixelGeometry)
) ?: throw RenderException("Cannot create surface")
canvas = surface!!.canvas
} else {
renderTarget = null
surface = null
canvas = null
}
}
override fun flush() {
// TODO: maybe make flush async as in JVM version.
super.flush()
surface?.flushAndSubmit()
metalRedrawer.finishFrame()
}
override fun rendererInfo(): String {
return "Native Metal: device ${metalRedrawer.device.name}"
}
}
package org.jetbrains.skiko.redrawer package org.jetbrains.skiko.redrawer
import kotlinx.cinterop.* import kotlinx.cinterop.*
import org.jetbrains.skia.BackendRenderTarget import org.jetbrains.skia.*
import org.jetbrains.skia.DirectContext import org.jetbrains.skiko.Logger
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.MetalContextHandler
import platform.CoreGraphics.CGColorCreate
import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB
import platform.CoreGraphics.CGContextRef
import platform.CoreGraphics.CGSizeMake
import platform.Foundation.NSRunLoop import platform.Foundation.NSRunLoop
import platform.Foundation.NSSelectorFromString import platform.Foundation.NSSelectorFromString
import platform.Metal.MTLCreateSystemDefaultDevice
import platform.Metal.MTLDeviceProtocol
import platform.Metal.MTLPixelFormatBGRA8Unorm
import platform.QuartzCore.* import platform.QuartzCore.*
import platform.darwin.* import platform.darwin.*
import kotlin.math.roundToInt
internal class MetalRedrawer( internal class MetalRedrawer(
private val layer: SkiaLayer private val metalLayer: CAMetalLayer,
) : Redrawer { private val drawIntoSurfaceCallback: (Surface) -> Unit
private val contextHandler = MetalContextHandler(layer) ) {
override val renderInfo: String get() = contextHandler.rendererInfo()
private var isDisposed = false private var isDisposed = false
internal val device = MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Metal is not supported on this system")
// Workaround for KN compiler bug
// Type mismatch: inferred type is objcnames.protocols.MTLDeviceProtocol but platform.Metal.MTLDeviceProtocol was expected
@Suppress("USELESS_CAST")
private val device = metalLayer.device as platform.Metal.MTLDeviceProtocol?
?: throw IllegalStateException("CAMetalLayer.device can not be null")
private val queue = device.newCommandQueue() ?: throw IllegalStateException("Couldn't create Metal command queue") private val queue = device.newCommandQueue() ?: throw IllegalStateException("Couldn't create Metal command queue")
private var currentDrawable: CAMetalDrawableProtocol? = null private val context = DirectContext.makeMetal(device.objcPtr(), queue.objcPtr())
private val metalLayer = MetalLayer()
// Semaphore for preventing command buffers count more than swapchain size to be scheduled/executed at the same time // Semaphore for preventing command buffers count more than swapchain size to be scheduled/executed at the same time
private val inflightSemaphore = dispatch_semaphore_create(metalLayer.maximumDrawableCount.toLong()) private val inflightSemaphore = dispatch_semaphore_create(metalLayer.maximumDrawableCount.toLong())
var maximumFramesPerSecond: NSInteger
get() = caDisplayLink.preferredFramesPerSecond
set(value) {
caDisplayLink.preferredFramesPerSecond = value
}
/* /*
* Indicates that scene is invalidated and next display link callback will draw * Indicates that scene is invalidated and next display link callback will draw
*/ */
...@@ -55,7 +54,7 @@ internal class MetalRedrawer( ...@@ -55,7 +54,7 @@ internal class MetalRedrawer(
if (hasScheduledDrawOnNextVSync) { if (hasScheduledDrawOnNextVSync) {
hasScheduledDrawOnNextVSync = false hasScheduledDrawOnNextVSync = false
drawIfLayerIsShowing() draw()
} }
if (!needsProactiveDisplayLink) { if (!needsProactiveDisplayLink) {
...@@ -67,47 +66,20 @@ internal class MetalRedrawer( ...@@ -67,47 +66,20 @@ internal class MetalRedrawer(
target = frameListener, target = frameListener,
selector = NSSelectorFromString(FrameTickListener::onDisplayLinkTick.name) selector = NSSelectorFromString(FrameTickListener::onDisplayLinkTick.name)
) )
init { init {
metalLayer.init(this.layer, contextHandler, device)
caDisplayLink.setPaused(true) caDisplayLink.setPaused(true)
caDisplayLink.addToRunLoop(NSRunLoop.mainRunLoop, NSRunLoop.mainRunLoop.currentMode) caDisplayLink.addToRunLoop(NSRunLoop.mainRunLoop, NSRunLoop.mainRunLoop.currentMode)
} }
fun makeContext() = DirectContext.makeMetal(device.objcPtr(), queue.objcPtr()) internal fun dispose() {
fun makeRenderTarget(width: Int, height: Int): BackendRenderTarget {
// If more than swapchain size count of command buffers are inflight
// wait until one finishes work
dispatch_semaphore_wait(inflightSemaphore, DISPATCH_TIME_FOREVER)
currentDrawable = metalLayer.nextDrawable()!!
return BackendRenderTarget.makeMetal(width, height, currentDrawable!!.texture.objcPtr())
}
override fun dispose() {
if (!isDisposed) { if (!isDisposed) {
caDisplayLink.invalidate() caDisplayLink.invalidate()
contextHandler.dispose()
metalLayer.dispose()
isDisposed = true isDisposed = true
} }
} }
override fun syncSize() { internal fun needRedraw() {
metalLayer.contentsScale = layer.contentScale.toDouble()
val osView = layer.view!!
val (w, h) = osView.frame.useContents {
size.width to size.height
}
metalLayer.frame = osView.frame
metalLayer.init(layer, contextHandler, device)
metalLayer.drawableSize = CGSizeMake(w * metalLayer.contentsScale, h * metalLayer.contentsScale)
osView.window?.screen?.maximumFramesPerSecond?.let {
caDisplayLink.preferredFramesPerSecond = it
}
}
override fun needRedraw() {
check(!isDisposed) { "MetalRedrawer is disposed" } check(!isDisposed) { "MetalRedrawer is disposed" }
hasScheduledDrawOnNextVSync = true hasScheduledDrawOnNextVSync = true
...@@ -116,86 +88,66 @@ internal class MetalRedrawer( ...@@ -116,86 +88,66 @@ internal class MetalRedrawer(
caDisplayLink.setPaused(false) caDisplayLink.setPaused(false)
} }
override fun redrawImmediately() {
// TODO: separate iOS MetalRedrawer from Redrawer, it's a false abstraction, iOS only uses MetalRedrawer explicitly
throw UnsupportedOperationException("This should never be called on iOS")
}
private fun drawIfLayerIsShowing() {
if (layer.isShowing()) {
draw()
}
}
private fun draw() { private fun draw() {
// TODO: maybe make flush async as in JVM version. if (isDisposed) {
autoreleasepool { //todo measure performance without autoreleasepool return
if (!isDisposed) {
contextHandler.draw()
}
} }
}
fun finishFrame() {
autoreleasepool { autoreleasepool {
currentDrawable?.let { val (width, height) = metalLayer.drawableSize.useContents {
val commandBuffer = queue.commandBuffer()!! width.roundToInt() to height.roundToInt()
commandBuffer.label = "Present"
commandBuffer.presentDrawable(it)
commandBuffer.addCompletedHandler {
// Signal work finish, allow a new command buffer to be scheduled
dispatch_semaphore_signal(inflightSemaphore)
}
commandBuffer.commit()
currentDrawable = null
} }
}
}
}
internal class MetalLayer : CAMetalLayer { if (width <= 0 || height <= 0) {
private lateinit var skiaLayer: SkiaLayer return@autoreleasepool
private lateinit var contextHandler: ContextHandler }
@OverrideInit
constructor() : super()
@OverrideInit
constructor(layer: Any) : super(layer)
fun init(
skiaLayer: SkiaLayer,
contextHandler: ContextHandler,
theDevice: MTLDeviceProtocol
) {
this.skiaLayer = skiaLayer
this.contextHandler = contextHandler
this.setNeedsDisplayOnBoundsChange(true)
this.removeAllAnimations()
// TODO: looks like a bug in K/N interop.
this.device = theDevice as objcnames.protocols.MTLDeviceProtocol?
this.pixelFormat = MTLPixelFormatBGRA8Unorm
this.contentsGravity = kCAGravityTopLeft
doubleArrayOf(0.0, 0.0, 0.0, 0.0).usePinned {
this.backgroundColor =
CGColorCreate(CGColorSpaceCreateDeviceRGB(), it.addressOf(0))
}
this.framebufferOnly = false
this.opaque = false // For UIKit interop through a "Hole"
skiaLayer.view?.let {
this.frame = it.frame
it.layer.addSublayer(this)
}
}
fun dispose() { dispatch_semaphore_wait(inflightSemaphore, DISPATCH_TIME_FOREVER)
this.removeFromSuperlayer()
// TODO: anything else to dispose the layer? val metalDrawable = metalLayer.nextDrawable()
}
if (metalDrawable == null) {
Logger.warn { "'metalLayer.nextDrawable()' returned null. 'metalLayer.allowsNextDrawableTimeout' should be set to false. Skipping the frame." }
dispatch_semaphore_signal(inflightSemaphore)
return@autoreleasepool
}
val renderTarget = BackendRenderTarget.makeMetal(width, height, metalDrawable.texture.objcPtr())
val surface = Surface.makeFromBackendRenderTarget(
context,
renderTarget,
SurfaceOrigin.TOP_LEFT,
SurfaceColorFormat.BGRA_8888,
ColorSpace.sRGB,
SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN)
)
if (surface == null) {
Logger.warn { "'Surface.makeFromBackendRenderTarget' returned null. Skipping the frame." }
renderTarget.close()
// TODO: manually release metalDrawable when K/N API arrives
dispatch_semaphore_signal(inflightSemaphore)
return@autoreleasepool
}
surface.canvas.clear(Color.WHITE)
drawIntoSurfaceCallback(surface)
surface.flushAndSubmit()
override fun drawInContext(ctx: CGContextRef?) { val commandBuffer = queue.commandBuffer()!!
contextHandler.draw() commandBuffer.label = "Present"
super.drawInContext(ctx) commandBuffer.presentDrawable(metalDrawable)
commandBuffer.addCompletedHandler {
// Signal work finish, allow a new command buffer to be scheduled
dispatch_semaphore_signal(inflightSemaphore)
}
commandBuffer.commit()
surface.close()
renderTarget.close()
// TODO manually release metalDrawable when K/N API arrives
}
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment