diff --git a/decorated-window-core/src/main/kotlin/io/github/kdroidfilter/nucleus/window/WindowControlArea.kt b/decorated-window-core/src/main/kotlin/io/github/kdroidfilter/nucleus/window/WindowControlArea.kt index 8704f5458..b96c05348 100644 --- a/decorated-window-core/src/main/kotlin/io/github/kdroidfilter/nucleus/window/WindowControlArea.kt +++ b/decorated-window-core/src/main/kotlin/io/github/kdroidfilter/nucleus/window/WindowControlArea.kt @@ -141,6 +141,8 @@ fun TitleBarScope.DialogCloseButton( ) { CompositionLocalProvider(LocalLayoutDirection provides LocalControlButtonsDirection.current) { val icons = linuxTitleBarIcons() + val layout = rememberLinuxButtonLayout() + val buttonAlignment = if (layout.controlsOnRight) Alignment.End else Alignment.Start val windowState = state.toDecoratedWindowState() val closeHover = if (windowState.isActive) icons.closeHoverFocused else icons.closeHover val closePressed = if (windowState.isActive) icons.closePressedFocused else icons.closePressed @@ -153,6 +155,7 @@ fun TitleBarScope.DialogCloseButton( iconPressed = closePressed, contentDescription = "Close", style = style, + alignment = buttonAlignment, isCloseButton = true, ) } diff --git a/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.Linux.kt b/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.Linux.kt index 2674d6627..b62339664 100644 --- a/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.Linux.kt +++ b/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.Linux.kt @@ -1,6 +1,5 @@ package io.github.kdroidfilter.nucleus.window -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.ExperimentalComposeUiApi @@ -10,11 +9,9 @@ import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp import com.jetbrains.JBR -import io.github.kdroidfilter.nucleus.core.runtime.LinuxDesktopEnvironment import io.github.kdroidfilter.nucleus.window.styling.TitleBarStyle +import io.github.kdroidfilter.nucleus.window.utils.linux.rememberLinuxButtonLayout import java.awt.event.MouseEvent @OptIn(ExperimentalComposeUiApi::class) @@ -31,7 +28,8 @@ internal fun DecoratedDialogScope.LinuxDialogTitleBar( val linuxStyle = createLinuxTitleBarStyle(style) val dialogState = state val controlDir = controlButtonsDirection.resolve() - val controlsSide = if (controlDir == LayoutDirection.Rtl) WindowControlsSide.Start else WindowControlsSide.End + val controlsOnRight = rememberLinuxButtonLayout().controlsOnRight + val controlsSide = if (controlsOnRight) WindowControlsSide.End else WindowControlsSide.Start CompositionLocalProvider(LocalWindowControlsSide provides controlsSide) { DialogTitleBarImpl( @@ -48,15 +46,7 @@ internal fun DecoratedDialogScope.LinuxDialogTitleBar( style = linuxStyle, controlButtonsDirection = controlDir, layoutPolicy = layoutPolicy, - applyTitleBar = { _, _ -> - val padding = - if (LinuxDesktopEnvironment.Current == LinuxDesktopEnvironment.KDE) { - PaddingValues(end = 4.dp) - } else { - PaddingValues(0.dp) - } - padding - }, + applyTitleBar = { _, _ -> kdePaddingForButtonLayout() }, ) { _ -> DialogCloseButton(window, dialogState, linuxStyle) content(dialogState) diff --git a/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.MacOS.kt b/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.MacOS.kt index 979ac9e26..3ce87af3f 100644 --- a/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.MacOS.kt +++ b/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.MacOS.kt @@ -29,7 +29,7 @@ internal fun DecoratedDialogScope.MacOSDialogTitleBar( val controlDir = controlButtonsDirection.resolve() val isRtl = controlDir == LayoutDirection.Rtl - val controlsSide = if (isRtl) WindowControlsSide.Start else WindowControlsSide.End + val controlsSide = if (isRtl) WindowControlsSide.End else WindowControlsSide.Start CompositionLocalProvider(LocalWindowControlsSide provides controlsSide) { DialogTitleBarImpl( diff --git a/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/TitleBar.MacOS.kt b/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/TitleBar.MacOS.kt index 93bef633a..03abc2c0e 100644 --- a/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/TitleBar.MacOS.kt +++ b/decorated-window-jbr/src/main/kotlin/io/github/kdroidfilter/nucleus/window/TitleBar.MacOS.kt @@ -93,7 +93,7 @@ internal fun DecoratedWindowScope.MacOSTitleBar( val controlDir = controlButtonsDirection.resolve() val controlIsRtl = controlDir == LayoutDirection.Rtl - val controlsSide = if (controlIsRtl) WindowControlsSide.Start else WindowControlsSide.End + val controlsSide = if (controlIsRtl) WindowControlsSide.End else WindowControlsSide.Start CompositionLocalProvider(LocalWindowControlsSide provides controlsSide) { TitleBarImpl( diff --git a/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.Linux.kt b/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.Linux.kt index 0bfb7bb1c..350595f90 100644 --- a/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.Linux.kt +++ b/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.Linux.kt @@ -1,6 +1,5 @@ package io.github.kdroidfilter.nucleus.window -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -13,10 +12,9 @@ import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import io.github.kdroidfilter.nucleus.core.runtime.LinuxDesktopEnvironment import io.github.kdroidfilter.nucleus.window.styling.TitleBarStyle import io.github.kdroidfilter.nucleus.window.utils.linux.JniLinuxWindowBridge +import io.github.kdroidfilter.nucleus.window.utils.linux.rememberLinuxButtonLayout import java.awt.MouseInfo @OptIn(ExperimentalComposeUiApi::class) @@ -31,7 +29,8 @@ internal fun DecoratedDialogScope.LinuxDialogTitleBar( content: @Composable TitleBarScope.(DecoratedDialogState) -> Unit = {}, ) { val controlDir = controlButtonsDirection.resolve() - val controlsSide = if (controlDir == LayoutDirection.Rtl) WindowControlsSide.Start else WindowControlsSide.End + val controlsOnRight = rememberLinuxButtonLayout().controlsOnRight + val controlsSide = if (controlsOnRight) WindowControlsSide.End else WindowControlsSide.Start if (JniLinuxWindowBridge.isLoaded) { NativeLinuxDialogTitleBar( @@ -80,15 +79,7 @@ private fun DecoratedDialogScope.NativeLinuxDialogTitleBar( style = linuxStyle, controlButtonsDirection = controlButtonsDirection, layoutPolicy = layoutPolicy, - applyTitleBar = { _, _ -> - val padding = - if (LinuxDesktopEnvironment.Current == LinuxDesktopEnvironment.KDE) { - PaddingValues(end = 4.dp) - } else { - PaddingValues(0.dp) - } - padding - }, + applyTitleBar = { _, _ -> kdePaddingForButtonLayout() }, backgroundContent = { Spacer( modifier = @@ -152,15 +143,7 @@ private fun DecoratedDialogScope.FallbackLinuxDialogTitleBar( style = linuxStyle, controlButtonsDirection = controlButtonsDirection, layoutPolicy = layoutPolicy, - applyTitleBar = { _, _ -> - val padding = - if (LinuxDesktopEnvironment.Current == LinuxDesktopEnvironment.KDE) { - PaddingValues(end = 4.dp) - } else { - PaddingValues(0.dp) - } - padding - }, + applyTitleBar = { _, _ -> kdePaddingForButtonLayout() }, backgroundContent = { Spacer(modifier = Modifier.fillMaxSize().windowDragHandler(window)) }, diff --git a/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.MacOS.kt b/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.MacOS.kt index 4d85faf93..1c72435c0 100644 --- a/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.MacOS.kt +++ b/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/DialogTitleBar.MacOS.kt @@ -27,7 +27,7 @@ internal fun DecoratedDialogScope.MacOSDialogTitleBar( ) { val controlDir = controlButtonsDirection.resolve() val controlIsRtl = controlDir == LayoutDirection.Rtl - val controlsSide = if (controlIsRtl) WindowControlsSide.Start else WindowControlsSide.End + val controlsSide = if (controlIsRtl) WindowControlsSide.End else WindowControlsSide.Start DisposableEffect(window) { onDispose { diff --git a/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/TitleBar.MacOS.kt b/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/TitleBar.MacOS.kt index e9c89d1f8..1c9439cf7 100644 --- a/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/TitleBar.MacOS.kt +++ b/decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/TitleBar.MacOS.kt @@ -97,7 +97,7 @@ internal fun DecoratedWindowScope.MacOSTitleBar( // correct side. Uses the control buttons direction (decoupled from content). val controlDir = controlButtonsDirection.resolve() val controlIsRtl = controlDir == LayoutDirection.Rtl - val controlsSide = if (controlIsRtl) WindowControlsSide.Start else WindowControlsSide.End + val controlsSide = if (controlIsRtl) WindowControlsSide.End else WindowControlsSide.Start LaunchedEffect(window, controlIsRtl) { val ptr = JniMacWindowUtil.getWindowPtr(window) if (ptr != 0L && JniMacTitleBarBridge.isLoaded) { diff --git a/plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/internal/configureJvmApplication.kt b/plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/internal/configureJvmApplication.kt index 724914f86..0543cfbda 100644 --- a/plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/internal/configureJvmApplication.kt +++ b/plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/internal/configureJvmApplication.kt @@ -708,7 +708,8 @@ private fun JvmApplicationContext.configurePackageTask( packageTask.packageDescription.set(executables.description) packageTask.packageCopyright.set(executables.copyright) packageTask.packageVendor.set(executables.vendor) - packageTask.packageVersion.set(packageVersionFor(packageTask.targetFormat)) + // jpackage app-image: use the jpackage-safe version. + packageTask.packageVersion.set(jpackageVersionFor(packageTask.targetFormat)) } val dirSuffix = if (sandboxed) "-sandboxed" else "" diff --git a/plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/internal/packageVersions.kt b/plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/internal/packageVersions.kt index e100fbfda..d373b4063 100644 --- a/plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/internal/packageVersions.kt +++ b/plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/internal/packageVersions.kt @@ -17,6 +17,21 @@ internal fun JvmApplicationContext.packageVersionFor(targetFormat: TargetFormat) ?: "1.0.0" } +/** + * Version used when jpackage builds the app-image (`--app-version`). + * + * jpackage is the only [io.github.kdroidfilter.nucleus.desktop.application.dsl.PackagingBackend.JPACKAGE] + * step ([TargetFormat.RawAppImage]) and enforces strict platform version rules — notably Windows + * rejects SemVer pre-release/build metadata such as `2.3.5-beta.7`. All real installer formats run + * through electron-builder and keep the full SemVer via [packageVersionFor]. + */ +internal fun JvmApplicationContext.jpackageVersionFor(targetFormat: TargetFormat): Provider = + packageVersionFor(targetFormat).map { it.toJpackageVersion() } + +// jpackage rejects SemVer pre-release/build metadata; keep only the MAJOR.MINOR.PATCH core. +private fun String.toJpackageVersion(): String = + substringBefore('-').substringBefore('+') + @Suppress("CyclomaticComplexMethod") // Exhaustive when on TargetFormat enum private fun JvmApplicationDistributions.packageVersionFor(targetFormat: TargetFormat): String? { val formatSpecificVersion: String? =