mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
Added browser slices support
This commit is contained in:
@@ -2,5 +2,6 @@ CEF integration for Desktop Jetpack Compose.
|
||||
|
||||
Run example:
|
||||
To run application execute in terminal: ``./gradlew run``
|
||||
To run application in browser sliced mode execute in terminal: ``./gradlew run --args="slices"``
|
||||
|
||||
PS. Mac OS X is currently not supported.
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.desktop.Window
|
||||
import androidx.compose.desktop.WindowEvents
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -22,18 +23,26 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.foundation.Text
|
||||
import org.jetbrains.compose.desktop.browser.BrowserState
|
||||
import org.jetbrains.compose.desktop.browser.CefView
|
||||
import org.jetbrains.compose.desktop.browser.Browser
|
||||
import org.jetbrains.compose.desktop.browser.BrowserView
|
||||
import org.jetbrains.compose.desktop.browser.BrowserSlicer
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val browser = when {
|
||||
args.isEmpty() -> BrowserView()
|
||||
args[0] == "slices" -> BrowserSlicer(IntSize(800, 700))
|
||||
else -> {
|
||||
BrowserView()
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val browser = BrowserState()
|
||||
val url = mutableStateOf("https://www.google.com")
|
||||
|
||||
Window(
|
||||
title = "CEF-compose",
|
||||
size = IntSize(800, 800),
|
||||
size = IntSize(900, 900),
|
||||
events = WindowEvents(
|
||||
onFocusGet = { browser.loadURL(url.value) }
|
||||
onFocusGet = { browser.load(url.value) }
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
@@ -50,7 +59,7 @@ fun main() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddressBar(browser: BrowserState, url: MutableState<String>) {
|
||||
private fun AddressBar(browser: Browser, url: MutableState<String>) {
|
||||
Surface(
|
||||
color = Color.Transparent,
|
||||
modifier = Modifier
|
||||
@@ -74,7 +83,7 @@ private fun AddressBar(browser: BrowserState, url: MutableState<String>) {
|
||||
Button(
|
||||
modifier = Modifier.preferredHeight(48.dp),
|
||||
shape = CircleShape,
|
||||
onClick = { browser.loadURL(url.value) }
|
||||
onClick = { browser.load(url.value) }
|
||||
) {
|
||||
Text(text = "Go!")
|
||||
}
|
||||
@@ -83,11 +92,24 @@ private fun AddressBar(browser: BrowserState, url: MutableState<String>) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WebView(browser: BrowserState) {
|
||||
private fun WebView(browser: Browser) {
|
||||
Surface(
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.fillMaxSize().padding(10.dp)
|
||||
) {
|
||||
CefView(browser)
|
||||
when (browser) {
|
||||
is BrowserView -> {
|
||||
browser.view()
|
||||
}
|
||||
is BrowserSlicer -> {
|
||||
Column {
|
||||
browser.slice(0, 200)
|
||||
Spacer(Modifier.height(30.dp))
|
||||
browser.slice(200, 200)
|
||||
Spacer(Modifier.height(30.dp))
|
||||
browser.tail()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.jetbrains.compose.desktop.browser
|
||||
|
||||
interface Browser {
|
||||
fun load(url: String)
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.jetbrains.compose.desktop.browser
|
||||
|
||||
import androidx.compose.desktop.AppManager
|
||||
import androidx.compose.desktop.AppFrame
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.MutableState
|
||||
import org.jetbrains.skija.Bitmap
|
||||
import org.jetbrains.skiko.HardwareLayer
|
||||
import javax.swing.JFrame
|
||||
import org.cef.CefApp
|
||||
|
||||
class BrowserState {
|
||||
private val url = mutableStateOf("")
|
||||
private val isReady = mutableStateOf(false)
|
||||
private lateinit var browser: CefBrowserWrapper
|
||||
|
||||
fun isReady(): Boolean {
|
||||
return isReady.value
|
||||
}
|
||||
|
||||
fun loadURL(url: String) {
|
||||
if (!this::browser.isInitialized) {
|
||||
val frame = AppManager.focusedWindow
|
||||
if (frame != null) {
|
||||
onActive(frame, url)
|
||||
}
|
||||
return
|
||||
}
|
||||
isReady.value = false
|
||||
browser.loadURL(url)
|
||||
isReady.value = true
|
||||
}
|
||||
|
||||
fun getBitmap(): Bitmap {
|
||||
return browser.getBitmap()
|
||||
}
|
||||
|
||||
fun onLayout(x: Int, y: Int, width: Int, height: Int) {
|
||||
browser.onLayout(x, y, width, height)
|
||||
}
|
||||
|
||||
fun onActive(frame: AppFrame, url: String) {
|
||||
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()
|
||||
|
||||
isReady.value = true
|
||||
}
|
||||
|
||||
fun onDismiss() {
|
||||
browser.onDismiss()
|
||||
}
|
||||
|
||||
fun setFocused(value: Boolean) {
|
||||
browser.setFocused(value)
|
||||
}
|
||||
|
||||
fun onInvalidate(onInvalidate: (() -> Unit)?) {
|
||||
browser.onInvalidate = onInvalidate
|
||||
}
|
||||
|
||||
private fun getHardwareLayer(window: JFrame): HardwareLayer? {
|
||||
val components = window.getContentPane().getComponents()
|
||||
for (component in components) {
|
||||
if (component is HardwareLayer) {
|
||||
return component
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
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.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.onDispose
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
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.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import java.awt.Component
|
||||
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 org.cef.CefApp
|
||||
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
|
||||
|
||||
//EXPERIMENTAL FOCUS API
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
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.foundation.clickable
|
||||
|
||||
class BrowserView : Browser {
|
||||
private lateinit var bitmap: MutableState<Bitmap>
|
||||
private lateinit var recomposer: MutableState<Any>
|
||||
internal var browser: CefBrowserWrapper? = null
|
||||
private val isReady = mutableStateOf(false)
|
||||
fun isReady(): Boolean {
|
||||
return isReady.value
|
||||
}
|
||||
|
||||
internal var location = IntOffset.Zero
|
||||
internal var size = IntSize.Zero
|
||||
|
||||
private var layout: BrowserLayout? = null
|
||||
|
||||
@Composable
|
||||
fun view() {
|
||||
if (isReady()) {
|
||||
invalidate()
|
||||
|
||||
layout = remember { BrowserLayout(this) }
|
||||
layout!!.view(bitmap.value, recomposer)
|
||||
}
|
||||
}
|
||||
|
||||
private var invalidated = false
|
||||
@Composable
|
||||
private fun invalidate() {
|
||||
if (!invalidated) {
|
||||
bitmap = remember { mutableStateOf(emptyBitmap) }
|
||||
recomposer = remember { mutableStateOf(Any()) }
|
||||
browser!!.onInvalidate = {
|
||||
bitmap.value = browser!!.getBitmap()
|
||||
recomposer.value = Any()
|
||||
}
|
||||
invalidated = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateBounds() {
|
||||
browser?.onLayout(location.x, location.y, 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()
|
||||
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) {
|
||||
if (isInLayer(event)) {
|
||||
browser?.onMouseEvent(event)
|
||||
}
|
||||
}
|
||||
override fun mouseReleased(event: MouseEvent) {
|
||||
if (isInLayer(event)) {
|
||||
browser?.onMouseEvent(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
layer.addMouseMotionListener(object : MouseMotionAdapter() {
|
||||
override fun mouseMoved(event: MouseEvent) {
|
||||
if (isInLayer(event)) {
|
||||
browser?.onMouseEvent(event)
|
||||
}
|
||||
}
|
||||
override fun mouseDragged(event: MouseEvent) {
|
||||
if (isInLayer(event)) {
|
||||
browser?.onMouseEvent(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
layer.addMouseWheelListener(object : MouseWheelListener {
|
||||
override fun mouseWheelMoved(event: MouseWheelEvent) {
|
||||
if (isInLayer(event)) {
|
||||
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): Boolean {
|
||||
if (
|
||||
event.x >= location.x &&
|
||||
event.x <= location.x + size.width &&
|
||||
event.y >= location.y &&
|
||||
event.y <= location.y + size.height
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private class BrowserLayout(val handler: BrowserView) {
|
||||
@OptIn(
|
||||
ExperimentalFocus::class,
|
||||
ExperimentalFoundationApi::class
|
||||
)
|
||||
@Composable
|
||||
fun view(bitmap: Bitmap, recomposer: MutableState<Any>) {
|
||||
val focusRequester = FocusRequester()
|
||||
|
||||
Box (
|
||||
modifier = Modifier.background(color = Color.White)
|
||||
.fillMaxSize()
|
||||
.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
handler.size = IntSize(placeable.width, placeable.height)
|
||||
handler.updateBounds()
|
||||
layout(placeable.width, placeable.height) {
|
||||
placeable.placeRelative(0, 0)
|
||||
}
|
||||
}
|
||||
.onGloballyPositioned { coordinates ->
|
||||
handler.location = IntOffset(
|
||||
coordinates.globalPosition.x.toInt(),
|
||||
coordinates.globalPosition.y.toInt()
|
||||
)
|
||||
}
|
||||
.focusRequester(focusRequester)
|
||||
.focus()
|
||||
.clickable(indication = null) { focusRequester.requestFocus() }
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier.size(handler.size.width.dp, handler.size.height.dp)
|
||||
) {
|
||||
drawIntoCanvas { canvas ->
|
||||
recomposer.value
|
||||
canvas.nativeCanvas.drawBitmapRect(
|
||||
bitmap,
|
||||
IRect(0, 0, handler.size.width.toInt(), handler.size.height.toInt()).toRect()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,22 @@
|
||||
package org.jetbrains.compose.desktop.browser
|
||||
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
|
||||
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 java.awt.KeyboardFocusManager
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
import org.cef.CefApp
|
||||
import org.cef.CefClient
|
||||
import org.cef.CefSettings
|
||||
import org.cef.browser.CefBrowser
|
||||
import org.cef.browser.BrowserView
|
||||
import org.cef.handler.CefFocusHandlerAdapter
|
||||
|
||||
import org.jetbrains.skija.Bitmap
|
||||
import org.jetbrains.skija.ImageInfo
|
||||
import org.jetbrains.skija.ColorAlphaType
|
||||
import org.jetbrains.skiko.HardwareLayer
|
||||
|
||||
class CefBrowserWrapper {
|
||||
private var offset = IntOffset(0, 0)
|
||||
private var isFocused = false
|
||||
private var cefFocus = true
|
||||
private val browser: BrowserView
|
||||
public var onInvalidate: (() -> Unit)? = null
|
||||
@@ -64,86 +52,17 @@ class CefBrowserWrapper {
|
||||
browser.onFocusLost()
|
||||
}
|
||||
})
|
||||
|
||||
layer.addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(event: MouseEvent) {
|
||||
if (isInLayer(event)) {
|
||||
browser.onMouseEvent(event)
|
||||
}
|
||||
}
|
||||
override fun mouseReleased(event: MouseEvent) {
|
||||
if (isInLayer(event)) {
|
||||
browser.onMouseEvent(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
layer.addMouseMotionListener(object : MouseMotionAdapter() {
|
||||
override fun mouseMoved(event: MouseEvent) {
|
||||
if (isInLayer(event)) {
|
||||
browser.onMouseEvent(event)
|
||||
}
|
||||
}
|
||||
override fun mouseDragged(event: MouseEvent) {
|
||||
if (isInLayer(event)) {
|
||||
browser.onMouseEvent(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
layer.addMouseWheelListener(object : MouseWheelListener {
|
||||
override fun mouseWheelMoved(event: MouseWheelEvent) {
|
||||
if (isInLayer(event)) {
|
||||
browser.onMouseScrollEvent(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
layer.addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(event: KeyEvent) {
|
||||
if (!isFocused) {
|
||||
return
|
||||
}
|
||||
browser.onKeyEvent(event)
|
||||
}
|
||||
override fun keyReleased(event: KeyEvent) {
|
||||
if (!isFocused) {
|
||||
return
|
||||
}
|
||||
browser.onKeyEvent(event)
|
||||
}
|
||||
override fun keyTyped(event: KeyEvent) {
|
||||
if (!isFocused) {
|
||||
return
|
||||
}
|
||||
browser.onKeyEvent(event)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun isInLayer(event: MouseEvent): Boolean {
|
||||
val x = event.x
|
||||
val y = event.y
|
||||
if (x > offset.x && y > offset.y) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun loadURL(url: String) {
|
||||
browser.loadURL(url)
|
||||
}
|
||||
|
||||
fun setFocused(value: Boolean) {
|
||||
isFocused = value
|
||||
}
|
||||
|
||||
fun getBitmap(): Bitmap {
|
||||
return browser.getBitmap()
|
||||
}
|
||||
|
||||
fun onLayout(x: Int, y: Int, width: Int, height: Int) {
|
||||
offset = IntOffset(x, y)
|
||||
browser.onResized(x, y, width, height)
|
||||
}
|
||||
|
||||
@@ -154,4 +73,25 @@ class CefBrowserWrapper {
|
||||
fun onDismiss() {
|
||||
CefApp.getInstance().dispose()
|
||||
}
|
||||
}
|
||||
|
||||
fun onMouseEvent(event: MouseEvent) {
|
||||
browser.onMouseEvent(event)
|
||||
}
|
||||
|
||||
fun onMouseScrollEvent(event: MouseWheelEvent) {
|
||||
browser.onMouseScrollEvent(event)
|
||||
}
|
||||
|
||||
fun onKeyEvent(event: KeyEvent) {
|
||||
if (cefFocus) {
|
||||
browser.onKeyEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val emptyBitmap: Bitmap
|
||||
get() {
|
||||
val bitmap = Bitmap()
|
||||
bitmap.allocPixels(ImageInfo.makeS32(1, 1, ColorAlphaType.PREMUL))
|
||||
return bitmap
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package org.jetbrains.compose.desktop.browser
|
||||
|
||||
import androidx.compose.desktop.AppWindowAmbient
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.emptyContent
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.onActive
|
||||
import androidx.compose.runtime.onDispose
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.globalPosition
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
import org.jetbrains.skija.IRect
|
||||
import org.jetbrains.skija.Bitmap
|
||||
import org.jetbrains.skija.ImageInfo
|
||||
import org.jetbrains.skija.ColorAlphaType
|
||||
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
//EXPERIMENTAL FOCUS API
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
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.foundation.clickable
|
||||
|
||||
private val width = mutableStateOf(0)
|
||||
private val height = mutableStateOf(0)
|
||||
private val x = mutableStateOf(0)
|
||||
private val y = mutableStateOf(0)
|
||||
private val emptyBitmap: Bitmap
|
||||
get() {
|
||||
val bitmap = Bitmap()
|
||||
bitmap.allocPixels(ImageInfo.makeS32(1, 1, ColorAlphaType.PREMUL))
|
||||
return bitmap
|
||||
}
|
||||
|
||||
@OptIn(
|
||||
ExperimentalFocus::class,
|
||||
ExperimentalFoundationApi::class
|
||||
)
|
||||
@Composable
|
||||
fun CefView(browser: BrowserState) {
|
||||
val bitmap = remember { mutableStateOf(emptyBitmap) }
|
||||
val forceRecompose = remember { mutableStateOf(Any()) }
|
||||
val focusRequester = FocusRequester()
|
||||
|
||||
if (browser.isReady()) {
|
||||
browser.onInvalidate {
|
||||
bitmap.value = browser.getBitmap()
|
||||
forceRecompose.value = Any()
|
||||
}
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
width.value = placeable.width
|
||||
height.value = placeable.height
|
||||
browser.onLayout(x.value, y.value, width.value, height.value)
|
||||
|
||||
layout(placeable.width, placeable.height) {
|
||||
placeable.placeRelative(0, 0)
|
||||
}
|
||||
}
|
||||
.onGloballyPositioned { coordinates ->
|
||||
x.value = coordinates.globalPosition.x.toInt()
|
||||
y.value = coordinates.globalPosition.y.toInt()
|
||||
}
|
||||
.focusRequester(focusRequester)
|
||||
.focusObserver { browser.setFocused(it.isFocused) }
|
||||
.focus()
|
||||
.clickable(indication = null) { focusRequester.requestFocus() }
|
||||
) {
|
||||
drawIntoCanvas { canvas ->
|
||||
forceRecompose.value
|
||||
bitmap.value
|
||||
canvas.nativeCanvas.drawBitmapRect(
|
||||
bitmap.value,
|
||||
IRect(0, 0, width.value, height.value).toRect()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDispose {
|
||||
browser.onDismiss()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user