feat(desktop): add native Windows window controller

This commit is contained in:
2026-07-22 15:49:11 +02:00
parent 0ab076e5b2
commit 0e97c014a1
4 changed files with 1357 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ dependencies {
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
implementation(libs.filekit.dialogs) implementation(libs.filekit.dialogs)
implementation(libs.jna.platform)
implementation(libs.kotlinx.coroutinesSwing) implementation(libs.kotlinx.coroutinesSwing)
implementation(libs.compose.uiToolingPreview) implementation(libs.compose.uiToolingPreview)

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,12 @@
package com.vnidrop.app package com.vnidrop.app
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowPlacement
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.jetbrains.skiko.GraphicsApi
class DesktopWindowChromeTest { class DesktopWindowChromeTest {
@Test @Test
@@ -34,4 +38,114 @@ class DesktopWindowChromeTest {
linuxWindowControlVisualState(isClose = false, isHovered = false, isPressed = false), linuxWindowControlVisualState(isClose = false, isHovered = false, isPressed = false),
) )
} }
@Test
fun windowsHitTestingPreservesNativeResizeAndCaptionBehavior() {
val geometry = windowsGeometry()
assertEquals(WindowsHitTestResult.TopLeft, windowsHitTest(2, 2, geometry))
assertEquals(WindowsHitTestResult.Right, windowsHitTest(1_198, 300, geometry))
assertEquals(WindowsHitTestResult.Bottom, windowsHitTest(600, 798, geometry))
assertEquals(WindowsHitTestResult.Caption, windowsHitTest(600, 24, geometry))
assertEquals(WindowsHitTestResult.Client, windowsHitTest(600, 200, geometry))
}
@Test
fun windowsCaptionButtonsReturnNativeHitCodesForSnapLayouts() {
val geometry = windowsGeometry()
assertEquals(WindowsHitTestResult.MinimizeButton, windowsHitTest(1_075, 20, geometry))
assertEquals(WindowsHitTestResult.MaximizeButton, windowsHitTest(1_120, 20, geometry))
assertEquals(WindowsHitTestResult.CloseButton, windowsHitTest(1_175, 20, geometry))
}
@Test
fun nativeCaptionStripIsSplitIntoThreeAdjacentButtons() {
val bounds = splitWindowsCaptionButtonBounds(Rect(646f, 0f, 793f, 30f))
assertEquals(Rect(646f, 0f, 695f, 30f), bounds.minimize)
assertEquals(Rect(695f, 0f, 744f, 30f), bounds.maximize)
assertEquals(Rect(744f, 0f, 793f, 30f), bounds.close)
}
@Test
fun extendedDwmTitleBarMarginScalesWithWindowDpi() {
assertEquals(48, windowsTitleBarHeightPixels(96))
assertEquals(60, windowsTitleBarHeightPixels(120))
assertEquals(72, windowsTitleBarHeightPixels(144))
}
@Test
fun maximizedWindowsDoNotExposeResizeBorders() {
val geometry = windowsGeometry().copy(isMaximized = true)
assertEquals(WindowsHitTestResult.Caption, windowsHitTest(2, 2, geometry))
}
@Test
fun restoredWindowsPaintThroughTheResizeBorderWhileMaximizedWindowsStayInset() {
assertEquals(
WindowsFrameInsets(),
windowsContentInsets(
isMaximized = false,
horizontalResizeBorder = 8,
verticalResizeBorder = 8,
),
)
assertEquals(
WindowsFrameInsets(left = 8, top = 9, right = 8, bottom = 9),
windowsContentInsets(
isMaximized = true,
horizontalResizeBorder = 8,
verticalResizeBorder = 9,
),
)
}
@Test
fun nativeMouseCoordinatesPreserveNegativeMonitorPositions() {
val x = -320
val y = -48
val packed = ((y and 0xffff).toLong() shl 16) or (x and 0xffff).toLong()
assertEquals(WindowsScreenPoint(x, y), windowsScreenPoint(packed))
}
@Test
fun nativeBackdropRequiresAGpuRendererWithTransparentSwapChainSupport() {
assertTrue(GraphicsApi.DIRECT3D.supportsWindowsTransparentBackground())
assertTrue(GraphicsApi.OPENGL.supportsWindowsTransparentBackground())
assertFalse(GraphicsApi.UNKNOWN.supportsWindowsTransparentBackground())
assertFalse(GraphicsApi.SOFTWARE_FAST.supportsWindowsTransparentBackground())
assertFalse(GraphicsApi.SOFTWARE_COMPAT.supportsWindowsTransparentBackground())
}
@Test
fun captionActionRequiresReleaseOnTheOriginallyPressedButton() {
assertTrue(
shouldActivateWindowsCaptionButton(
WindowsCaptionButton.Maximize,
WindowsCaptionButton.Maximize,
),
)
assertFalse(
shouldActivateWindowsCaptionButton(
WindowsCaptionButton.Close,
WindowsCaptionButton.Maximize,
),
)
assertFalse(shouldActivateWindowsCaptionButton(WindowsCaptionButton.Close, null))
}
private fun windowsGeometry() = WindowsHitTestGeometry(
width = 1_200,
height = 800,
horizontalResizeBorder = 8,
verticalResizeBorder = 8,
isMaximized = false,
caption = Rect(0f, 0f, 1_200f, 48f),
minimizeButton = Rect(1_062f, 8f, 1_108f, 40f),
maximizeButton = Rect(1_108f, 8f, 1_154f, 40f),
closeButton = Rect(1_154f, 8f, 1_200f, 40f),
)
} }

View File

@@ -13,6 +13,7 @@ androidx-testExt = "1.3.0"
composeMultiplatform = "1.11.1" composeMultiplatform = "1.11.1"
filekit = "0.14.2" filekit = "0.14.2"
gobley = "0.3.7" gobley = "0.3.7"
jna = "5.19.1"
junit = "4.13.2" junit = "4.13.2"
kotlin = "2.4.0" kotlin = "2.4.0"
kotlinx-coroutines = "1.11.0" kotlinx-coroutines = "1.11.0"
@@ -42,6 +43,7 @@ compose-uiTest = { module = "org.jetbrains.compose.ui:ui-test", version.ref = "c
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" } compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
filekit-dialogs = { module = "io.github.vinceglb:filekit-dialogs", version.ref = "filekit" } filekit-dialogs = { module = "io.github.vinceglb:filekit-dialogs", version.ref = "filekit" }
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
kotlinx-coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }