JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.2k stars 1.17k forks source link

Application freezes on resize if JCEF is initialized before Compose #2939

Closed ScottPierce closed 1 month ago

ScottPierce commented 1 year ago

Describe the bug If JCEF is loaded before a Compose Window is displayed, the Compose window then cannot be resized, and the application will crash if a resize is attempted.

Affected platforms Select one of the platforms below:

Versions

To Reproduce Use the following library for JCEF: https://github.com/jcefmaven/jcefmaven

Steps and/or the code snippet to reproduce the behavior:

  1. In the application main function load jcef first thing:
        CefAppBuilder().apply {
            setInstallDir(DesktopApp.jcefDir)
        }.build()
  2. Load Compose Window
  3. Try to resize the window, notice that it freezes, and can't recover

Expected behavior That window resizing works no matter when jcef is loaded.

Additional context Window resizing appears to work if you load JCEF after the compose window is created.

AlexeyTsvetkov commented 1 year ago

Reproduced in the following environment:

I've made a reproducible sample (see below). For convenience, there are three Gradle tasks:

  1. runInitJcefBeforeComposeApplication, which initializes jcef before Compose application {} is initialized. This case freezes on resize or on when the window is closed.
  2. runInitJcefAfterComposeApplication, which initializes jcef right after Compose application {} is initialized.
  3. runInitJcefAfterComposeWindow, which initializes jcef right after Compose Window() is called.

Only in the first case the app freezes on resize or when the window is closed.

// build.gradle.kts
import org.gradle.jvm.tasks.Jar

plugins {
    kotlin("multiplatform") version "1.8.10"
    id("org.jetbrains.compose") version "1.3.1"
}

repositories {
    google()
    mavenCentral()
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

kotlin {
    jvm {
        jvmToolchain(11)
        withJava()
    }
    sourceSets {
        val jvmMain by getting {
            dependencies {
                implementation("me.friwi:jcefmaven:110.0.25")
                implementation(compose.desktop.currentOs)
            }
        }
        val jvmTest by getting
    }
}

fun JavaExec.cofigureRunJcef() {
    mainClass.set("MainKt")
    val kotlinJvm = kotlin.jvm()
    val compilation = kotlinJvm.compilations.getByName("main")
    val jar = tasks.getByName(kotlinJvm.artifactsTaskName)

    dependsOn(jar)
    classpath((jar as Jar).archiveFile.get().asFile)
    classpath(compilation.runtimeDependencyFiles)
}

tasks.register("runInitJcefBeforeComposeApplication", JavaExec::class) {
    cofigureRunJcef()
    args("before-compose-application")
}

tasks.register("runInitJcefAfterComposeApplication", JavaExec::class) {
    cofigureRunJcef()
    args("after-compose-application")
}

tasks.register("runInitJcefAfterComposeWindow", JavaExec::class) {
    cofigureRunJcef()
    args("after-compose-window")
}
// src/jvmMain/kotlin/Main.kt
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import me.friwi.jcefmaven.CefAppBuilder

@Composable
@Preview
fun App() {
    MaterialTheme {
        var clicks by remember { mutableStateOf(0) }
        Row {
            Button(onClick = { clicks++ }) {
                Text("$clicks clicks")
            }
        }
    }
}

fun String.toDashCase(): String =
    buildString {
        for ((index, char) in this@toDashCase.withIndex()) {
            if (index > 0 && char.isUpperCase()) {
                append("-")
            }
            append(char.lowercaseChar())
        }
    }

enum class InitLocation {
    BeforeComposeApplication, AfterComposeApplication, AfterComposeWindow;

    companion object {
        fun fromDashCaseName(dashCaseName: String?): InitLocation {
            if (dashCaseName == null) return values().first()

            val initModeByDashCaseName = values().associateBy { it.name.toDashCase() }
            return initModeByDashCaseName.getOrElse(dashCaseName) {
                error("'$dashCaseName' is not a valid value for jcef init location. Valid values are: ${initModeByDashCaseName.keys.joinToString(", ") { "'$it'" }}")
            }
        }
    }
}

fun main(args: Array<String>) {
    val requestedInitLocation = InitLocation.fromDashCaseName(args.firstOrNull())
    fun initJcefAt(location: InitLocation) {
        if (location == requestedInitLocation) {
            System.out.println("Initializing jcef at: ${location.name.toDashCase()}")
            CefAppBuilder().build()
        }
    }

    initJcefAt(InitLocation.BeforeComposeApplication)

    application {
        initJcefAt(InitLocation.AfterComposeApplication)

        Window(onCloseRequest = ::exitApplication) {
            initJcefAt(InitLocation.AfterComposeWindow)
            App()
        }
    }
}
okushnikov commented 2 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.