| /* |
| * 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.compose.animation.core.AnimationClockObserver |
| import androidx.compose.runtime.dispatch.DesktopUiDispatcher |
| import androidx.ui.text.platform.paragraphActualFactory |
| import androidx.ui.text.platform.paragraphIntrinsicsActualFactory |
| import com.jogamp.opengl.GL |
| import com.jogamp.opengl.GLAutoDrawable |
| import com.jogamp.opengl.GLCapabilities |
| import com.jogamp.opengl.GLEventListener |
| import com.jogamp.opengl.GLProfile |
| import com.jogamp.opengl.awt.GLCanvas |
| import org.jetbrains.skija.BackendRenderTarget |
| import org.jetbrains.skija.Canvas |
| import org.jetbrains.skija.ColorSpace |
| import org.jetbrains.skija.Context |
| import org.jetbrains.skija.FramebufferFormat |
| import org.jetbrains.skija.Library |
| import org.jetbrains.skija.Surface |
| import org.jetbrains.skija.SurfaceColorFormat |
| import org.jetbrains.skija.SurfaceOrigin |
| import java.awt.event.KeyAdapter |
| import java.awt.event.KeyEvent |
| import java.awt.event.MouseAdapter |
| import java.awt.event.MouseEvent |
| import java.awt.event.MouseMotionAdapter |
| import java.nio.IntBuffer |
| import javax.swing.JDialog |
| import javax.swing.JFrame |
| |
| private class SkijaState { |
| var context: Context? = null |
| var renderTarget: BackendRenderTarget? = null |
| var surface: Surface? = null |
| var canvas: Canvas? = null |
| var textureId: Int = 0 |
| val intBuf1 = IntBuffer.allocate(1) |
| |
| fun clear() { |
| if (surface != null) { |
| surface!!.close() |
| } |
| if (renderTarget != null) { |
| renderTarget!!.close() |
| } |
| } |
| } |
| |
| interface SkiaRenderer { |
| fun onInit() |
| fun onRender(canvas: Canvas, width: Int, height: Int) |
| fun onReshape(canvas: Canvas, width: Int, height: Int) |
| fun onDispose() |
| |
| fun onMouseClicked(x: Int, y: Int, modifiers: Int) |
| fun onMousePressed(x: Int, y: Int, modifiers: Int) |
| fun onMouseReleased(x: Int, y: Int, modifiers: Int) |
| fun onMouseDragged(x: Int, y: Int, modifiers: Int) |
| |
| fun onKeyTyped(char: Char) |
| fun onKeyPressed(code: Int, char: Char) |
| fun onKeyReleased(code: Int, char: Char) |
| } |
| |
| class Window : JFrame, SkiaFrame { |
| companion object { |
| init { |
| initCompose() |
| } |
| } |
| |
| override val parent: AppFrame |
| override val glCanvas: GLCanvas |
| override var renderer: SkiaRenderer? = null |
| override val vsync = true |
| |
| constructor(width: Int, height: Int, parent: AppFrame) : super() { |
| this.parent = parent |
| setSize(width, height) |
| } |
| |
| init { |
| glCanvas = initCanvas(this, vsync) |
| glCanvas.setSize(width, height) |
| contentPane.add(glCanvas) |
| size = contentPane.preferredSize |
| } |
| } |
| |
| class Dialog : JDialog, SkiaFrame { |
| @OptIn(androidx.compose.ui.text.android.InternalPlatformTextApi::class) |
| companion object { |
| init { |
| Library.load("/", "skija") |
| // Until https://github.com/Kotlin/kotlinx.coroutines/issues/2039 is resolved |
| // we have to set this property manually for coroutines to work. |
| System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false") |
| |
| @Suppress("DEPRECATION_ERROR") |
| paragraphIntrinsicsActualFactory = ::DesktopParagraphIntrinsics |
| @Suppress("DEPRECATION_ERROR") |
| paragraphActualFactory = ::DesktopParagraph |
| } |
| } |
| |
| override val parent: AppFrame |
| override val glCanvas: GLCanvas |
| override var renderer: SkiaRenderer? = null |
| override val vsync = true |
| |
| constructor( |
| attached: JFrame?, |
| width: Int, |
| height: Int, |
| parent: AppFrame |
| ) : super(attached, true) { |
| this.parent = parent |
| setSize(width, height) |
| } |
| |
| init { |
| glCanvas = initCanvas(this, vsync) |
| glCanvas.setSize(width, height) |
| contentPane.add(glCanvas) |
| size = contentPane.preferredSize |
| } |
| } |
| |
| internal interface SkiaFrame { |
| val parent: AppFrame |
| val glCanvas: GLCanvas |
| var renderer: SkiaRenderer? |
| val vsync: Boolean |
| |
| fun close() { |
| glCanvas.destroy() |
| } |
| } |
| |
| private fun initSkija( |
| glCanvas: GLCanvas, |
| skijaState: SkijaState, |
| vsync: Boolean, |
| reinitTexture: Boolean |
| ) { |
| with(skijaState) { |
| val width = glCanvas.width |
| val height = glCanvas.height |
| val dpiX = glCanvas.nativeSurface.surfaceWidth.toFloat() / width |
| val dpiY = glCanvas.nativeSurface.surfaceHeight.toFloat() / height |
| if (vsync) glCanvas.gl.setSwapInterval(1) |
| skijaState.clear() |
| val intBuf1 = IntBuffer.allocate(1) |
| glCanvas.gl.glGetIntegerv(GL.GL_DRAW_FRAMEBUFFER_BINDING, intBuf1) |
| val fbId = intBuf1[0] |
| renderTarget = BackendRenderTarget.makeGL( |
| (width * dpiX).toInt(), |
| (height * dpiY).toInt(), |
| 0, |
| 8, |
| fbId, |
| FramebufferFormat.GR_GL_RGBA8 |
| ) |
| surface = Surface.makeFromBackendRenderTarget( |
| context, |
| renderTarget, |
| SurfaceOrigin.BOTTOM_LEFT, |
| SurfaceColorFormat.RGBA_8888, |
| ColorSpace.getSRGB() |
| ) |
| canvas = surface!!.canvas |
| canvas!!.scale(dpiX, dpiY) |
| if (reinitTexture) { |
| glCanvas.gl.glGetIntegerv(GL.GL_TEXTURE_BINDING_2D, intBuf1) |
| skijaState.textureId = intBuf1[0] |
| } |
| } |
| } |
| |
| // 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) |
| // We cannot rely on double buffering. |
| capabilities.doubleBuffered = false |
| val glCanvas = GLCanvas(capabilities) |
| |
| val skijaState = SkijaState() |
| |
| glCanvas.addMouseListener(object : MouseAdapter() { |
| override fun mouseClicked(event: MouseEvent) { |
| frame.renderer!!.onMouseClicked(event.x, event.y, event.getModifiersEx()) |
| } |
| |
| override fun mousePressed(event: MouseEvent) { |
| frame.renderer!!.onMousePressed(event.x, event.y, event.getModifiersEx()) |
| } |
| |
| override fun mouseReleased(event: MouseEvent) { |
| frame.renderer!!.onMouseReleased(event.x, event.y, event.getModifiersEx()) |
| } |
| }) |
| glCanvas.addMouseMotionListener(object : MouseMotionAdapter() { |
| override fun mouseDragged(event: MouseEvent) { |
| frame.renderer!!.onMouseDragged(event.x, event.y, event.getModifiersEx()) |
| } |
| }) |
| glCanvas.addKeyListener(object : KeyAdapter() { |
| override fun keyPressed(event: KeyEvent) { |
| frame.renderer!!.onKeyPressed(event.keyCode, event.keyChar) |
| } |
| |
| override fun keyReleased(event: KeyEvent) { |
| frame.renderer!!.onKeyReleased(event.keyCode, event.keyChar) |
| } |
| |
| override fun keyTyped(event: KeyEvent) { |
| frame.renderer!!.onKeyTyped(event.keyChar) |
| } |
| }) |
| glCanvas.addGLEventListener(object : GLEventListener { |
| override fun reshape( |
| drawable: GLAutoDrawable?, |
| x: Int, |
| y: Int, |
| width: Int, |
| height: Int |
| ) { |
| initSkija(glCanvas, skijaState, vsync, false) |
| frame.renderer!!.onReshape(skijaState.canvas!!, width, height) |
| } |
| |
| override fun init(drawable: GLAutoDrawable?) { |
| skijaState.context = Context.makeGL() |
| initSkija(glCanvas, skijaState, vsync, false) |
| frame.renderer!!.onInit() |
| } |
| |
| override fun dispose(drawable: GLAutoDrawable?) { |
| frame.renderer!!.onDispose() |
| AppManager.removeWindow(frame.parent) |
| } |
| |
| override fun display(drawable: GLAutoDrawable) { |
| skijaState.apply { |
| val gl = drawable.gl!! |
| canvas!!.clear(0xFFFFFFF) |
| gl.glBindTexture(GL.GL_TEXTURE_2D, textureId) |
| frame.renderer!!.onRender( |
| canvas!!, glCanvas.width, glCanvas.height |
| ) |
| context!!.flush() |
| gl.glGetIntegerv(GL.GL_TEXTURE_BINDING_2D, intBuf1) |
| textureId = intBuf1[0] |
| if (vsync) gl.glFinish() |
| } |
| } |
| }) |
| |
| return glCanvas |
| } |
| |
| 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) |
| } |
| } |
| } |
| } |