I have implemented a JavaFX WebView
in Compose Desktop (additional features of a WebView
here - http://tutorials.jenkov.com/javafx/webview.html). My actual usecase was for mapping purposes as there is not really any decent mapping Java libraries. However this example just loads a web url, but you can easily load a custom html
page from resources that loads custom content.
Setup as follows (note java fx plugin can only be used in actual desktop module not shared as it clashes with android library plugin).
desktop module build.gradle
file :
import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
kotlin("multiplatform")
id("org.openjfx.javafxplugin") version "0.0.10"
id("org.jetbrains.compose") version "1.0.1"
}
group = "com.example.webview"
version = "1.0.0"
kotlin {
jvm {
compilations.all {
kotlinOptions {
jvmTarget = "11"
}
withJava()
}
}
sourceSets {
val jvmMain by getting {
dependencies {
implementation(project(":shared"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt")
{ version { strictly("1.6.0-native-mt") } }
implementation(compose.desktop.currentOs)
//Optional other deps
implementation(compose.uiTooling)
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.animation)
implementation(compose.animationGraphics)
}
}
val jvmTest by getting {
dependencies {
// testing deps
}
}
}
}
compose.desktop {
application {
mainClass = "Mainkt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "your.package.name"
packageVersion = "1.0.0"
}
}
}
javafx {
version = "16"
modules = listOf("javafx.controls", "javafx.swing", "javafx.web", "javafx.graphics")
}
Main
kotlin file :
fun main() = application(exitProcessOnExit = true) {
// Required to make sure the JavaFx event loop doesn't finish (can happen when java fx panels in app are shown/hidden)
val finishListener = object : PlatformImpl.FinishListener {
override fun idle(implicitExit: Boolean) {}
override fun exitCalled() {}
}
PlatformImpl.addListener(finishListener)
Window(
title = "WebView Test",
resizable = false,
state = WindowState(
placement = Floating,
size = DpSize(minWidth.dp, minHeight.dp)
),
onCloseRequest = {
PlatformImpl.removeListener(finishListener)
exitApplication()
},
content = {
val jfxPanel = remember { JFXPanel() }
var jsObject = remember<JSObject?> { null }
Box(modifier = Modifier.fillMaxSize().background(Color.White)) {
ComposeJFXPanel(
composeWindow = window,
jfxPanel = jfxPanel,
onCreate = {
Platform.runLater {
val root = WebView()
val engine = root.engine
val scene = Scene(root)
engine.loadWorker.stateProperty().addListener { _, _, newState ->
if (newState === Worker.State.SUCCEEDED) {
jsObject = root.engine.executeScript("window") as JSObject
// execute other javascript / setup js callbacks fields etc..
}
}
engine.loadWorker.exceptionProperty().addListener { _, _, newError ->
println("page load error : $newError")
}
jfxPanel.scene = scene
engine.load("http://google.com") // can be a html document from resources ..
engine.setOnError { error -> println("onError : $error") }
}
}, onDestroy = {
Platform.runLater {
jsObject?.let { jsObj ->
// clean up code for more complex implementations i.e. removing javascript callbacks etc..
}
}
})
}
})
}
@Composable
fun ComposeJFXPanel(
composeWindow: ComposeWindow,
jfxPanel: JFXPanel,
onCreate: () -> Unit,
onDestroy: () -> Unit = {}
) {
val jPanel = remember { JPanel() }
val density = LocalDensity.current.density
Layout(
content = {},
modifier = Modifier.onGloballyPositioned { childCoordinates ->
val coordinates = childCoordinates.parentCoordinates!!
val location = coordinates.localToWindow(Offset.Zero).round()
val size = coordinates.size
jPanel.setBounds(
(location.x / density).toInt(),
(location.y / density).toInt(),
(size.width / density).toInt(),
(size.height / density).toInt()
)
jPanel.validate()
jPanel.repaint()
},
measurePolicy = { _, _ -> layout(0, 0) {} })
DisposableEffect(jPanel) {
composeWindow.add(jPanel)
jPanel.layout = BorderLayout(0, 0)
jPanel.add(jfxPanel)
onCreate()
onDispose {
onDestroy()
composeWindow.remove(jPanel)
}
}
}
Result :
Thanks goes to code in this original post which gives the bulk implementation of JFXPanel
- https://github.com/JetBrains/compose-jb/issues/519#issuecomment-804030550 and subsequent comment by myself around the Java FX event loop fix.