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.01k stars 1.17k forks source link

Buttons are not displayed under MacOS #968

Closed Genie23 closed 3 years ago

Genie23 commented 3 years ago

Hello,

I am a French developer (not very comfortable with English) of small software, and I want to develop an application in Kotlin that can be used on Windows, Linux and macOS. Very logically I tested this framework, and with virtual machines I tested the compilation to these different platforms.

Now that I was able to test that it worked, I chose this solution for my next two software developments. But when I wanted to test the dialog boxes I'll need (one for file selection and one for confirmation) on the different target OS to see how it would look like, I noticed a completely different bug: I created a window with a menu and 3 buttons. On Windows, where I develop, everything works fine. On my Debian virtual machine, it also works fine. I have an error on my Fedora, but maybe it comes from another library I use (JOptionPane, for the display of the confirmation dialog box).

But what brings me today is the behavior on macOS. Indeed, my buttons are simply not displayed. If I click where they should be, the action executes well, but they don't display themselves.

Here is what I expect as a result (screen taken on Windows):

Rendering under Windows with the buttons

And what I get on macOS:

Rendering under macOS with buttons not displayed

And here is, for all intents and purposes, my build.gradle.kts :

import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.5.10"
    id("org.jetbrains.compose") version "0.4.0"
}

group = "fr.genie23"
version = "1.0"

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

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-reflect:1.5.10")
    implementation(compose.desktop.currentOs)
    implementation("org.apache.pdfbox:pdfbox:2.0.24")
    implementation("org.apache.pdfbox:fontbox:2.0.24")
    implementation("org.apache.pdfbox:jempbox:1.8.16")
    implementation("org.apache.pdfbox:xmpbox:2.0.24")
    implementation("org.apache.pdfbox:preflight:2.0.24")
    implementation("org.apache.pdfbox:pdfbox-tools:2.0.24")
    implementation("org.bouncycastle:bcprov-jdk15on:1.69")
    implementation("org.bouncycastle:bcmail-jdk15on:1.69")
    implementation("org.bouncycastle:bcpkix-jdk15on:1.69")
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "11"
}

compose.desktop {
    application {
        mainClass = "MainKt"
        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Exe, TargetFormat.Deb, TargetFormat.Rpm)
            windows {
                packageName = "PDF Assembler"
                exePackageVersion = "1.0.0"
                iconFile.set(project.file("src/main/resources/Logo.64x64.ico"))
                dirChooser = true
                menuGroup = "Genie23.fr"
            }
            macOS {
                packageName = "PDF Assembler"
                dmgPackageVersion = "1.0.0"
                iconFile.set(project.file("src/main/resources/Logo.64x64.icns"))
                bundleID = "fr.genie23.pdf-assembler"
            }
            linux {
                packageName = "pdf-assembler"
                debPackageVersion = "1.0.0"
                rpmPackageVersion = "1.0.0"
                iconFile.set(project.file("src/main/resources/Logo.64x64.png"))
                debMaintainer = "admin@genie23.fr"
                menuGroup = "Genie23.fr"
                appCategory = "FileTools"
                rpmLicenseType = "CC-BY-ND"
            }
        }
    }
}

tasks.register<Copy>("copyRelease") {
    group = "compose desktop"
    description = "Copy release builded executable into out folder."
    duplicatesStrategy = DuplicatesStrategy.INCLUDE
    from("build/compose/binaries/main/deb", "build/compose/binaries/main/dmg", "build/compose/binaries/main/exe", "build/compose/binaries/main/rpm")
    into("out")
}

tasks.withType<KotlinCompile>().configureEach {
    kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}

And my source code src/kotlin/main.kt :

import androidx.compose.desktop.AppManager
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.v1.Menu
import androidx.compose.ui.window.v1.MenuBar
import androidx.compose.ui.window.v1.MenuItem
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
import javax.swing.JFileChooser
import javax.swing.JOptionPane
import javax.swing.KeyStroke
import javax.swing.UIManager
import javax.swing.filechooser.FileNameExtensionFilter

lateinit var pdfManagement:PDFManagement

fun main() {
    // Active OpenGL pour contourner l'incompatibilité avec les chipsets intel
    System.setProperty("skiko.renderApi", "OPENGL")

    /*
    System.setProperty("apple.laf.useScreenMenuBar", "true");
    System.setProperty("com.apple.mrj.application.apple.menu.about.name", "WikiTeX");
    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    */

    constructMainWindow()
}

@OptIn(ExperimentalComposeUiApi::class)
fun constructMainWindow() {
    MainWindow()
}

@ExperimentalComposeUiApi
class MainWindow() {
    init {
        Window (
            title = "PDF Assembler",
            icon = loadImageResource("Logo.64x64.png"),
            size = IntSize(1280,720),
            menuBar = MenuBar(
                Menu(
                    name = "Fichier",
                    MenuItem(
                        name = "À propos",
                        shortcut = KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0),
                        onClick = {
                            Window (
                                title = "Aide de PDF Assembler",
                                icon = loadImageResource("Help.64x64.png"),
                                size = IntSize(640,480),
                            ) {
                                Text("Aide !")
                            }
                        }
                    ),
                    MenuItem(
                        name = "Quitter",
                        shortcut = KeyStroke.getKeyStroke(KeyEvent.VK_F4, ActionEvent.ALT_MASK),
                        onClick = {
                            AppManager.exit()
                        }
                    )
                )
            )
        ) {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Column {
                    Button(
                        onClick = {
                            val files = fileDialog(
                                isLoad = true,
                                extensions = listOf(FileNameExtensionFilter("Fichiers PDF", "pdf")),
                                title = "Sélectionner des documents à fusionner"
                            )
                            if (files != null) {
                                for(file in files) {
                                    println(file.absolutePath)
                                }
                            }
                        }
                    ) {
                        Text("Ajouter un fichier")
                    }
                    Button(
                        onClick = {
                            val files = fileDialog(
                                isLoad = false,
                                extensions = listOf(
                                    FileNameExtensionFilter("Fichiers PDF", "pdf"),
                                    FileNameExtensionFilter("Fichiers Word", "doc", "docx")
                                ),
                                title = "Sauvegarder le résultat de la fusion"
                            )
                            if (files != null) {
                                for(file in files) {
                                    println(file.absolutePath)
                                }
                            }
                        }
                    ) {
                        Text("Sauvegarder le résultat")
                    }
                    Button(
                        onClick = {
                            val files = fileDialog(
                                isLoad = true,
                                title = "Ajouter n'importe quoi"
                            )
                            if (files != null) {
                                for(file in files) {
                                    println(file.absolutePath)
                                }
                            }
                        }
                    ) {
                        Text("N'importe quoi")
                    }
                }
            }
        }
    }

    private fun confirm(title:String = "", message:String = "", type:Int = JOptionPane.INFORMATION_MESSAGE):Boolean {
        return JOptionPane.showConfirmDialog(null, message, title, JOptionPane.YES_NO_OPTION, type) == JOptionPane.OK_OPTION;
    }

    private fun fileDialog(isLoad:Boolean = false, title:String = "Sélectionnez un fichier", extensions:List<FileNameExtensionFilter>? = null):List<File>? {
        val dialog = JFileChooser()
        dialog.dialogTitle = title
        dialog.isMultiSelectionEnabled = isLoad
        if(extensions != null) {
            dialog.isAcceptAllFileFilterUsed = false
            for(extension in extensions) {
                dialog.addChoosableFileFilter(extension)
            }
        }
        if((if(isLoad) dialog.showOpenDialog(null) else dialog.showSaveDialog(null)) == JFileChooser.APPROVE_OPTION) {
            if (isLoad) {
                return dialog.selectedFiles.filterNot { !it.canRead() }
            } else {
                // Récupération de la première extension du filtre courant
                val extension = (dialog.fileFilter as FileNameExtensionFilter).extensions.get(0)
                if(!dialog.fileFilter.accept(dialog.selectedFile)) {
                    dialog.selectedFile = File(dialog.selectedFile.absolutePath + ".$extension")
                }
                if(dialog.selectedFile.canRead()) {
                    if(confirm(title = "Fichier existant", message = "Le fichier ${dialog.selectedFile.name} existe déjà. Voulez-vous le remplacer ?", type = JOptionPane.WARNING_MESSAGE)) {
                        return listOf(dialog.selectedFile)
                    }
                    else {
                        return null
                    }
                } else {
                    return listOf(dialog.selectedFile)
                }
            }
        }
        return null
    }
}

@Suppress("SameParameterValue")
private fun loadImageResource(path: String): BufferedImage {
    val resource = Thread.currentThread().contextClassLoader.getResource(path)
    requireNotNull(resource) { "Resource $path not found" }
    return resource.openStream().use(ImageIO::read)
}

The three commented lines in the main function :

    System.setProperty("apple.laf.useScreenMenuBar", "true");
    System.setProperty("com.apple.mrj.application.apple.menu.about.name", "WikiTeX");
    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

are three lines that I tried to unblock the situation but that didn't change anything on macOS (I tested both with and without).

Thank you for all the help you can give me.

akurasov commented 3 years ago

Hi! And does the simplest window with one button without any logic shows fine on your Mac?

Genie23 commented 3 years ago

With this window containing only one button (I couldn't make it simpler I think) :

        Window (
            title = "PDF Assembler",
            icon = loadImageResource("Logo.64x64.png"),
            size = IntSize(1280,720),
        ) {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Button(
                    onClick = {
                        println("Debug");
                    }
                ) {
                    Text("Ajouter un fichier")
                }
            }
        }

The result on macOS : bug composer desktop 03

The result of the ./gradlew runDistributable command (I clicked several times in the middle of my window, where the button should have been) :

Starting a Gradle Daemon, 1 incompatible and 1 stopped Daemons could not be reused, use --status for details

> Task :createDistributable
The distribution is written to /Users/cedric/pdf-assembler/build/compose/binaries/main/app

> Task :runDistributable
WARNING: GL pipe is running in software mode (Renderer ID=0x1020400)
Debug
Debug
Debug
Debug
Debug               
BUILD SUCCESSFUL in 2m 59sING [1m 36s]

I specify that there is still an onclick because it is mandatory (I tried without, I was insulted by the compiler).

akurasov commented 3 years ago

I tried the same code(only with icon = loadImageResource("Logo.64x64.png"), commented out, since I don't have this icon) on my Mac and everything works fine.

Let's try to use the latest Compose/Kotlin version

plugins { kotlin("jvm") version "1.5.21" id("org.jetbrains.compose") version "1.0.0-alpha1-rc1" }

and see if it helps. And also try to comment this line with icon.

Rsedaikin commented 3 years ago

@Genie23 I see that you are using VMWare, try to enable "Accelerate 3D graphics" in the virtual machine settings (the guest OS must be completely turned off) image

or set software renderer while you work on your project System.setProperty("skiko.renderApi", "SOFTWARE")

import androidx.compose.material.Text
import androidx.compose.material.Button
import androidx.compose.runtime.*
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() {
    System.setProperty("skiko.renderApi", "SOFTWARE")
    application {
        var text by remember { mutableStateOf("Hello, World!") }
        Window(onCloseRequest = ::exitApplication) {
            Button(onClick = {
                text = "Hello, Desktop!"
            }) {
                Text(text)
            }
        }
    }
}

if you do not want change your code you can set environment variable SKIKO_RENDER_API="SOFTWARE"

Genie23 commented 3 years ago

So, after updating the version of Kotlin and Compose I was using, I replaced my main.kt with the following (I had several "deprecated" errors, and it still didn't work) :

import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    // Active OpenGL pour contourner l'incompatibilité avec les chipsets intel
    System.setProperty("skiko.renderApi", "OPENGL")

    Window(onCloseRequest = ::exitApplication, title = "Editor") {
        Button(onClick = {
            println("Debug")
        }) {
            Text("Test")
        }
    }
}

Still the same result, my button still doesn't display on macOS.

Here is the trace of the gradlew runDistributable task :


> Task :runDistributable
WARNING: GL pipe is running in software mode (Renderer ID=0x1020400)

BUILD SUCCESSFUL in 13s
8 actionable tasks: 1 executed, 7 up-to-date

The parameters of my virtual machine : bug composer desktop 04

And the property skiko.renderApi, I set it already in my program, on OPENGL, because otherwise I have an error because of my Intel graphics chipset :

> Task :runDistributable
Failed Direct3D call. Error: 0x887a0005
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_UNCAUGHT_CXX_EXCEPTION (0xe06d7363) at pc=0x00007ffcbfbe4ed9, pid=20080, tid=24860
#
# JRE version: OpenJDK Runtime Environment (16.0.2+7) (build 16.0.2+7-67)
# Java VM: OpenJDK 64-Bit Server VM (16.0.2+7-67, mixed mode, tiered, compressed oops, compressed class ptrs, g1 gc, windows-amd64)
# Problematic frame:
# C  [KERNELBASE.dll+0x34ed9]
#
# No core dump will be written. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# D:\Utilisateurs\sphin\Documents\Kotlin_Projects\PDF_Assembler\build\compose\binaries\main\app\PDF Assembler\hs_err_pid20080.log
#
# If you would like to submit a bug report, please visit:
#   https://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#

> Task :runDistributable FAILED

Execution failed for task ':runDistributable'.
> Process 'command 'D:\Utilisateurs\sphin\Documents\Kotlin_Projects\PDF_Assembler\build\compose\binaries\main\app\PDF Assembler\PDF Assembler.exe'' finished with non-zero exit value 1

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
Genie23 commented 3 years ago

With the skiko.renderApi option set to SOFTWARE, indeed, my button is displayed (and I don't have the error I have on windows, but maybe VMWare uses my N card instead of my Intel chipset) :

image

What do I have to install on my macOS virtual machine for OPENGL to work? Because I think that some macOS users might have a graphics chipset incompatible with Compose (to my knowledge, it only concerns Intel chipsets).

And if not, how can I test the OS to run my System.setProperty only on Windows and Linux (so all except macOS) to get around this problem ?

Rsedaikin commented 3 years ago

on mac os you should use "METAL" renderer instead of "OPENGL", try to set to Metal renderer for mac os (only work on macos)

Genie23 commented 3 years ago

"METAL" also works. Also, because "SOFTWARE" must be the default option I guess, so the one incompatible with some Intel GPUs (or maybe all, after all I don't know, I tested only one), here is the code of my main.kt adapted to the 3 OS :

import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

enum class OS {
    WINDOWS, LINUX, MAC, SOLARIS
}

fun getOS(): OS? {
    val os = System.getProperty("os.name").toLowerCase()
    return when {
        os.contains("win") -> {
            OS.WINDOWS
        }
        os.contains("nix") || os.contains("nux") || os.contains("aix") -> {
            OS.LINUX
        }
        os.contains("mac") -> {
            OS.MAC
        }
        os.contains("sunos") -> {
            OS.SOLARIS
        }
        else -> null
    }
}

fun main() = application {
    // Détermine le système de rendu en fonction de l'OS
    when(getOS()) {
        OS.LINUX, OS.WINDOWS -> System.setProperty("skiko.renderApi", "OPENGL");
        OS.MAC -> System.setProperty("skiko.renderApi", "METAL");
    }

    Window(onCloseRequest = ::exitApplication, title = "Editor") {
        Button(onClick = {
            println("Debug")
        }) {
            Text("Test")
        }
    }
}

And the rendering :

So my concern seems to be solved. I just have to study the new tutorials to realize my current cross-platform application project.

Thanks to you :)

okushnikov commented 1 month ago

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