Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2020 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | package androidx.ui.desktop |
| 17 | |
Louis Pullen-Freilich | 5bb0c471 | 2020-07-20 22:01:15 +0100 | [diff] [blame] | 18 | import androidx.compose.animation.core.AnimationClockObserver |
Louis Pullen-Freilich | 4b2e7e0 | 2020-07-21 17:26:40 +0100 | [diff] [blame] | 19 | import androidx.compose.runtime.dispatch.DesktopUiDispatcher |
Andrei Rudenko | 2d7eb40 | 2020-06-11 11:11:52 +0200 | [diff] [blame] | 20 | import androidx.ui.text.platform.paragraphActualFactory |
| 21 | import androidx.ui.text.platform.paragraphIntrinsicsActualFactory |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 22 | import com.jogamp.opengl.GL |
| 23 | import com.jogamp.opengl.GLAutoDrawable |
| 24 | import com.jogamp.opengl.GLCapabilities |
| 25 | import com.jogamp.opengl.GLEventListener |
| 26 | import com.jogamp.opengl.GLProfile |
| 27 | import com.jogamp.opengl.awt.GLCanvas |
Andrei Rudenko | 2d7eb40 | 2020-06-11 11:11:52 +0200 | [diff] [blame] | 28 | import org.jetbrains.skija.BackendRenderTarget |
| 29 | import org.jetbrains.skija.Canvas |
| 30 | import org.jetbrains.skija.ColorSpace |
| 31 | import org.jetbrains.skija.Context |
Igor Demin | 4a0e9f0 | 2020-06-26 17:58:39 +0300 | [diff] [blame] | 32 | import org.jetbrains.skija.FramebufferFormat |
| 33 | import org.jetbrains.skija.Library |
Andrei Rudenko | 2d7eb40 | 2020-06-11 11:11:52 +0200 | [diff] [blame] | 34 | import org.jetbrains.skija.Surface |
Igor Demin | 4a0e9f0 | 2020-06-26 17:58:39 +0300 | [diff] [blame] | 35 | import org.jetbrains.skija.SurfaceColorFormat |
| 36 | import org.jetbrains.skija.SurfaceOrigin |
Igor Demin | b3f9d00 | 2020-07-09 19:29:41 +0300 | [diff] [blame] | 37 | import java.awt.event.KeyAdapter |
| 38 | import java.awt.event.KeyEvent |
| 39 | import java.awt.event.MouseAdapter |
| 40 | import java.awt.event.MouseEvent |
| 41 | import java.awt.event.MouseMotionAdapter |
| 42 | import java.nio.IntBuffer |
| 43 | import javax.swing.JDialog |
| 44 | import javax.swing.JFrame |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 45 | |
| 46 | private class SkijaState { |
| 47 | var context: Context? = null |
| 48 | var renderTarget: BackendRenderTarget? = null |
| 49 | var surface: Surface? = null |
| 50 | var canvas: Canvas? = null |
Nikolay Igotti | 479275b | 2020-04-29 11:32:00 +0300 | [diff] [blame] | 51 | var textureId: Int = 0 |
| 52 | val intBuf1 = IntBuffer.allocate(1) |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 53 | |
| 54 | fun clear() { |
| 55 | if (surface != null) { |
| 56 | surface!!.close() |
| 57 | } |
| 58 | if (renderTarget != null) { |
| 59 | renderTarget!!.close() |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | interface SkiaRenderer { |
| 65 | fun onInit() |
| 66 | fun onRender(canvas: Canvas, width: Int, height: Int) |
Nikolay Igotti | e7b9f02 | 2020-05-12 22:47:57 +0300 | [diff] [blame] | 67 | fun onReshape(canvas: Canvas, width: Int, height: Int) |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 68 | fun onDispose() |
Nikolay Igotti | 40e5412 | 2020-05-11 21:30:52 +0300 | [diff] [blame] | 69 | |
| 70 | fun onMouseClicked(x: Int, y: Int, modifiers: Int) |
| 71 | fun onMousePressed(x: Int, y: Int, modifiers: Int) |
| 72 | fun onMouseReleased(x: Int, y: Int, modifiers: Int) |
| 73 | fun onMouseDragged(x: Int, y: Int, modifiers: Int) |
Nikolay Igotti | 1741739 | 2020-05-15 14:56:57 +0300 | [diff] [blame] | 74 | |
| 75 | fun onKeyTyped(char: Char) |
| 76 | fun onKeyPressed(code: Int, char: Char) |
| 77 | fun onKeyReleased(code: Int, char: Char) |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 78 | } |
| 79 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 80 | class Window : JFrame, SkiaFrame { |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 81 | companion object { |
| 82 | init { |
Igor Demin | b3f9d00 | 2020-07-09 19:29:41 +0300 | [diff] [blame] | 83 | initCompose() |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 84 | } |
| 85 | } |
| 86 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 87 | override val parent: AppFrame |
| 88 | override val glCanvas: GLCanvas |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 89 | override var renderer: SkiaRenderer? = null |
Andrey Rudenko | 6921eaa | 2020-07-03 13:41:17 +0200 | [diff] [blame] | 90 | override val vsync = true |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 91 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 92 | constructor(width: Int, height: Int, parent: AppFrame) : super() { |
| 93 | this.parent = parent |
| 94 | setSize(width, height) |
| 95 | } |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 96 | |
| 97 | init { |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 98 | glCanvas = initCanvas(this, vsync) |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 99 | glCanvas.setSize(width, height) |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 100 | contentPane.add(glCanvas) |
| 101 | size = contentPane.preferredSize |
| 102 | } |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 103 | } |
| 104 | |
| 105 | class Dialog : JDialog, SkiaFrame { |
Louis Pullen-Freilich | a2be36b | 2020-07-21 21:35:11 +0100 | [diff] [blame^] | 106 | @OptIn(androidx.compose.ui.text.android.InternalPlatformTextApi::class) |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 107 | companion object { |
| 108 | init { |
| 109 | Library.load("/", "skija") |
| 110 | // Until https://github.com/Kotlin/kotlinx.coroutines/issues/2039 is resolved |
| 111 | // we have to set this property manually for coroutines to work. |
| 112 | System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false") |
| 113 | |
| 114 | @Suppress("DEPRECATION_ERROR") |
| 115 | paragraphIntrinsicsActualFactory = ::DesktopParagraphIntrinsics |
| 116 | @Suppress("DEPRECATION_ERROR") |
| 117 | paragraphActualFactory = ::DesktopParagraph |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | override val parent: AppFrame |
| 122 | override val glCanvas: GLCanvas |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 123 | override var renderer: SkiaRenderer? = null |
Andrey Rudenko | 6921eaa | 2020-07-03 13:41:17 +0200 | [diff] [blame] | 124 | override val vsync = true |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 125 | |
| 126 | constructor( |
| 127 | attached: JFrame?, |
| 128 | width: Int, |
| 129 | height: Int, |
| 130 | parent: AppFrame |
| 131 | ) : super(attached, true) { |
| 132 | this.parent = parent |
| 133 | setSize(width, height) |
| 134 | } |
| 135 | |
| 136 | init { |
| 137 | glCanvas = initCanvas(this, vsync) |
| 138 | glCanvas.setSize(width, height) |
| 139 | contentPane.add(glCanvas) |
| 140 | size = contentPane.preferredSize |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | internal interface SkiaFrame { |
| 145 | val parent: AppFrame |
| 146 | val glCanvas: GLCanvas |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 147 | var renderer: SkiaRenderer? |
| 148 | val vsync: Boolean |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 149 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 150 | fun close() { |
| 151 | glCanvas.destroy() |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | private fun initSkija( |
| 156 | glCanvas: GLCanvas, |
| 157 | skijaState: SkijaState, |
| 158 | vsync: Boolean, |
| 159 | reinitTexture: Boolean |
| 160 | ) { |
| 161 | with(skijaState) { |
| 162 | val width = glCanvas.width |
| 163 | val height = glCanvas.height |
| 164 | val dpiX = glCanvas.nativeSurface.surfaceWidth.toFloat() / width |
| 165 | val dpiY = glCanvas.nativeSurface.surfaceHeight.toFloat() / height |
| 166 | if (vsync) glCanvas.gl.setSwapInterval(1) |
| 167 | skijaState.clear() |
| 168 | val intBuf1 = IntBuffer.allocate(1) |
| 169 | glCanvas.gl.glGetIntegerv(GL.GL_DRAW_FRAMEBUFFER_BINDING, intBuf1) |
| 170 | val fbId = intBuf1[0] |
| 171 | renderTarget = BackendRenderTarget.makeGL( |
| 172 | (width * dpiX).toInt(), |
| 173 | (height * dpiY).toInt(), |
| 174 | 0, |
| 175 | 8, |
| 176 | fbId, |
| 177 | FramebufferFormat.GR_GL_RGBA8 |
| 178 | ) |
| 179 | surface = Surface.makeFromBackendRenderTarget( |
| 180 | context, |
| 181 | renderTarget, |
| 182 | SurfaceOrigin.BOTTOM_LEFT, |
| 183 | SurfaceColorFormat.RGBA_8888, |
| 184 | ColorSpace.getSRGB() |
| 185 | ) |
| 186 | canvas = surface!!.canvas |
| 187 | canvas!!.scale(dpiX, dpiY) |
| 188 | if (reinitTexture) { |
| 189 | glCanvas.gl.glGetIntegerv(GL.GL_TEXTURE_BINDING_2D, intBuf1) |
| 190 | skijaState.textureId = intBuf1[0] |
Nikolay Igotti | 65fb036 | 2020-04-07 15:03:05 +0300 | [diff] [blame] | 191 | } |
| 192 | } |
| 193 | } |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 194 | |
Andrey Rudenko | 6921eaa | 2020-07-03 13:41:17 +0200 | [diff] [blame] | 195 | // Simple FPS tracker for debug purposes |
| 196 | internal class FPSTracker { |
| 197 | private var t0 = 0L |
| 198 | private val times = DoubleArray(155) |
| 199 | private var timesIdx = 0 |
| 200 | |
| 201 | fun track() { |
| 202 | val t1 = System.nanoTime() |
| 203 | times[timesIdx] = (t1 - t0) / 1000000.0 |
| 204 | t0 = t1 |
| 205 | timesIdx = (timesIdx + 1) % times.size |
| 206 | println("FPS: ${1000 / times.takeWhile { it > 0 }.average()}") |
| 207 | } |
| 208 | } |
| 209 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 210 | private fun initCanvas(frame: SkiaFrame, vsync: Boolean = false): GLCanvas { |
| 211 | val profile = GLProfile.get(GLProfile.GL3) |
| 212 | val capabilities = GLCapabilities(profile) |
| 213 | // We cannot rely on double buffering. |
| 214 | capabilities.doubleBuffered = false |
| 215 | val glCanvas = GLCanvas(capabilities) |
| 216 | |
| 217 | val skijaState = SkijaState() |
| 218 | |
| 219 | glCanvas.addMouseListener(object : MouseAdapter() { |
| 220 | override fun mouseClicked(event: MouseEvent) { |
| 221 | frame.renderer!!.onMouseClicked(event.x, event.y, event.getModifiersEx()) |
| 222 | } |
Igor Demin | b3f9d00 | 2020-07-09 19:29:41 +0300 | [diff] [blame] | 223 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 224 | override fun mousePressed(event: MouseEvent) { |
| 225 | frame.renderer!!.onMousePressed(event.x, event.y, event.getModifiersEx()) |
| 226 | } |
Igor Demin | b3f9d00 | 2020-07-09 19:29:41 +0300 | [diff] [blame] | 227 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 228 | override fun mouseReleased(event: MouseEvent) { |
| 229 | frame.renderer!!.onMouseReleased(event.x, event.y, event.getModifiersEx()) |
| 230 | } |
| 231 | }) |
| 232 | glCanvas.addMouseMotionListener(object : MouseMotionAdapter() { |
| 233 | override fun mouseDragged(event: MouseEvent) { |
| 234 | frame.renderer!!.onMouseDragged(event.x, event.y, event.getModifiersEx()) |
| 235 | } |
| 236 | }) |
| 237 | glCanvas.addKeyListener(object : KeyAdapter() { |
| 238 | override fun keyPressed(event: KeyEvent) { |
| 239 | frame.renderer!!.onKeyPressed(event.keyCode, event.keyChar) |
| 240 | } |
Igor Demin | b3f9d00 | 2020-07-09 19:29:41 +0300 | [diff] [blame] | 241 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 242 | override fun keyReleased(event: KeyEvent) { |
| 243 | frame.renderer!!.onKeyReleased(event.keyCode, event.keyChar) |
| 244 | } |
Igor Demin | b3f9d00 | 2020-07-09 19:29:41 +0300 | [diff] [blame] | 245 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 246 | override fun keyTyped(event: KeyEvent) { |
| 247 | frame.renderer!!.onKeyTyped(event.keyChar) |
| 248 | } |
| 249 | }) |
| 250 | glCanvas.addGLEventListener(object : GLEventListener { |
| 251 | override fun reshape( |
| 252 | drawable: GLAutoDrawable?, |
| 253 | x: Int, |
| 254 | y: Int, |
| 255 | width: Int, |
| 256 | height: Int |
| 257 | ) { |
| 258 | initSkija(glCanvas, skijaState, vsync, false) |
| 259 | frame.renderer!!.onReshape(skijaState.canvas!!, width, height) |
| 260 | } |
Igor Demin | b3f9d00 | 2020-07-09 19:29:41 +0300 | [diff] [blame] | 261 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 262 | override fun init(drawable: GLAutoDrawable?) { |
| 263 | skijaState.context = Context.makeGL() |
| 264 | initSkija(glCanvas, skijaState, vsync, false) |
| 265 | frame.renderer!!.onInit() |
| 266 | } |
Igor Demin | b3f9d00 | 2020-07-09 19:29:41 +0300 | [diff] [blame] | 267 | |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 268 | override fun dispose(drawable: GLAutoDrawable?) { |
| 269 | frame.renderer!!.onDispose() |
| 270 | AppManager.removeWindow(frame.parent) |
| 271 | } |
Igor Demin | b3f9d00 | 2020-07-09 19:29:41 +0300 | [diff] [blame] | 272 | |
Andrey Rudenko | 6921eaa | 2020-07-03 13:41:17 +0200 | [diff] [blame] | 273 | override fun display(drawable: GLAutoDrawable) { |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 274 | skijaState.apply { |
Andrey Rudenko | 6921eaa | 2020-07-03 13:41:17 +0200 | [diff] [blame] | 275 | val gl = drawable.gl!! |
Roman Sedaikin | eb45e98b | 2020-06-26 13:17:03 +0300 | [diff] [blame] | 276 | canvas!!.clear(0xFFFFFFF) |
| 277 | gl.glBindTexture(GL.GL_TEXTURE_2D, textureId) |
| 278 | frame.renderer!!.onRender( |
| 279 | canvas!!, glCanvas.width, glCanvas.height |
| 280 | ) |
| 281 | context!!.flush() |
| 282 | gl.glGetIntegerv(GL.GL_TEXTURE_BINDING_2D, intBuf1) |
| 283 | textureId = intBuf1[0] |
| 284 | if (vsync) gl.glFinish() |
| 285 | } |
| 286 | } |
| 287 | }) |
| 288 | |
| 289 | return glCanvas |
Andrey Rudenko | 6921eaa | 2020-07-03 13:41:17 +0200 | [diff] [blame] | 290 | } |
| 291 | |
| 292 | internal class DesktopAnimationClock(fps: Int, val dispatcher: DesktopUiDispatcher) : |
| 293 | BaseAnimationClock() { |
| 294 | val delay = 1_000 / fps |
| 295 | |
| 296 | @Volatile |
| 297 | private var scheduled = false |
| 298 | private fun frameCallback(time: Long) { |
| 299 | scheduled = false |
| 300 | dispatchTime(time / 1000000) |
| 301 | } |
| 302 | |
| 303 | override fun subscribe(observer: AnimationClockObserver) { |
| 304 | super.subscribe(observer) |
| 305 | scheduleIfNeeded() |
| 306 | } |
| 307 | |
| 308 | override fun dispatchTime(frameTimeMillis: Long) { |
| 309 | super.dispatchTime(frameTimeMillis) |
| 310 | scheduleIfNeeded() |
| 311 | } |
| 312 | |
| 313 | private fun scheduleIfNeeded() { |
| 314 | when { |
| 315 | scheduled -> return |
| 316 | !hasObservers() -> return |
| 317 | else -> { |
| 318 | scheduled = true |
| 319 | dispatcher.scheduleCallbackWithDelay(delay, ::frameCallback) |
| 320 | } |
| 321 | } |
| 322 | } |
| 323 | } |