You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

299 lines
9.7 KiB

package org.jetbrains.compose.desktop.browser
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.AppFrame
import androidx.compose.foundation.background
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.focus
import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
import androidx.compose.ui.focusRequester
import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.globalPosition
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.IntSize
import java.awt.Component
import java.awt.Point
import java.awt.event.KeyEvent
import java.awt.event.KeyAdapter
import java.awt.event.KeyListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.MouseListener
import java.awt.event.MouseMotionListener
import java.awt.event.MouseWheelEvent
import java.awt.event.MouseWheelListener
import java.awt.event.MouseMotionAdapter
import javax.swing.JFrame
import org.jetbrains.skija.IRect
import org.jetbrains.skija.Bitmap
import org.jetbrains.skija.ImageInfo
import org.jetbrains.skija.ColorAlphaType
import org.jetbrains.skiko.HardwareLayer
class BrowserSlicer(val size: IntSize) : Browser {
private lateinit var bitmap: MutableState<Bitmap>
private lateinit var recomposer: MutableState<Any>
private var browser: CefBrowserWrapper? = null
private val isReady = mutableStateOf(false)
fun isReady(): Boolean {
return isReady.value
}
private var slices = mutableListOf<BrowserSlice>()
private var tail: BrowserSlice? = null
private var entire: BrowserSlice? = null
@Composable
fun full() {
if (isReady()) {
invalidate()
entire = remember { BrowserSlice(this, 0, size.height) }
entire!!.view(bitmap.value, recomposer)
}
}
@Composable
fun slice(offset: Int, height: Int) {
if (isReady()) {
invalidate()
val slice = BrowserSlice(this, offset, height)
slices.add(slice)
slice.view(bitmap.value, recomposer)
}
}
@Composable
fun tail() {
if (isReady()) {
invalidate()
var offset = 0
for (slice in slices) {
val bottom = slice.offset + slice.height
if (offset < bottom) {
offset = bottom
}
}
tail = remember { BrowserSlice(this, offset, size.height - offset) }
tail!!.view(bitmap.value, recomposer)
}
}
fun updateSize(size: IntSize) {
browser?.onLayout(0, 0, size.width, size.height)
}
override fun load(url: String) {
if (browser == null) {
val frame = AppManager.focusedWindow
if (frame != null) {
val window = frame.window
if (!window.isVisible()) {
return
}
var layer = getHardwareLayer(window)
if (layer == null) {
throw Error("Browser initialization failed!")
}
browser = CefBrowserWrapper(
startURL = url,
layer = layer
)
browser?.onActive()
updateSize(size)
addListeners(layer)
isReady.value = true
}
return
}
browser?.loadURL(url)
isReady.value = true
}
fun dismiss() {
browser?.onDismiss()
}
private fun getHardwareLayer(window: JFrame): HardwareLayer? {
val components = window.getContentPane().getComponents()
for (component in components) {
if (component is HardwareLayer) {
return component
}
}
return null
}
private fun addListeners(layer: Component) {
layer.addMouseListener(object : MouseAdapter() {
override fun mousePressed(event: MouseEvent) {
val slice = isInLayer(event)
if (slice != null) {
event.translatePoint(-slice.x, -slice.y + slice.offset)
browser?.onMouseEvent(event)
}
}
override fun mouseReleased(event: MouseEvent) {
val slice = isInLayer(event)
if (slice != null) {
event.translatePoint(-slice.x, -slice.y + slice.offset)
browser?.onMouseEvent(event)
}
}
})
layer.addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseMoved(event: MouseEvent) {
val slice = isInLayer(event)
if (slice != null) {
event.translatePoint(-slice.x, -slice.y + slice.offset)
browser?.onMouseEvent(event)
}
}
override fun mouseDragged(event: MouseEvent) {
val slice = isInLayer(event)
if (slice != null) {
event.translatePoint(-slice.x, -slice.y + slice.offset)
browser?.onMouseEvent(event)
}
}
})
layer.addMouseWheelListener(object : MouseWheelListener {
override fun mouseWheelMoved(event: MouseWheelEvent) {
val slice = isInLayer(event)
if (slice != null) {
event.translatePoint(-slice.x, -slice.y + slice.offset)
browser?.onMouseScrollEvent(event)
}
}
})
layer.addKeyListener(object : KeyAdapter() {
override fun keyPressed(event: KeyEvent) {
browser?.onKeyEvent(event)
}
override fun keyReleased(event: KeyEvent) {
browser?.onKeyEvent(event)
}
override fun keyTyped(event: KeyEvent) {
browser?.onKeyEvent(event)
}
})
}
private fun isInLayer(event: MouseEvent): BrowserSlice? {
if (entire != null && isHovered(event.point, entire!!)) {
return entire
}
if (tail != null && isHovered(event.point, tail!!)) {
return tail
}
for (slice in slices) {
if (isHovered(event.point, slice)) {
return slice
}
}
return null
}
private fun isHovered(point: Point, slice: BrowserSlice): Boolean {
if (
point.x >= slice.x &&
point.x <= slice.x + size.width &&
point.y >= slice.y &&
point.y <= slice.y + slice.height
) {
return true
}
return false
}
internal fun getBitmap(): Bitmap {
return browser!!.getBitmap()
}
private var invalidated = false
@Composable
private fun invalidate() {
if (!invalidated) {
bitmap = remember { mutableStateOf(emptyBitmap) }
recomposer = remember { mutableStateOf(Any()) }
browser!!.onInvalidate = {
bitmap.value = getBitmap()
recomposer.value = Any()
}
invalidated = true
}
}
}
private class BrowserSlice(val handler: BrowserSlicer, val offset: Int, val height: Int) {
var x: Int = 0
private set
var y: Int = 0
private set
@OptIn(
ExperimentalFocus::class,
ExperimentalFoundationApi::class
)
@Composable
fun view(bitmap: Bitmap, recomposer: MutableState<Any>) {
val focusRequester = FocusRequester()
Box (
modifier = Modifier.background(color = Color.White)
.size(handler.size.width.dp, height.dp)
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(handler.size.width, height) {
placeable.placeRelative(0, 0)
}
}
.onGloballyPositioned { coordinates ->
x = coordinates.globalPosition.x.toInt()
y = coordinates.globalPosition.y.toInt()
}
.focusRequester(focusRequester)
.focus()
.clickable(indication = null) { focusRequester.requestFocus() }
) {
Canvas(
modifier = Modifier.size(handler.size.width.dp, height.dp)
) {
drawIntoCanvas { canvas ->
recomposer.value
canvas.nativeCanvas.drawBitmapIRect(
bitmap,
IRect(0, offset, handler.size.width, offset + height),
IRect(0, 0, handler.size.width, height).toRect()
)
}
}
}
}
}