mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
Merge pull request #12 from vnidrop/feat/macos-unified-window-chrome
feat(desktop): unify macOS window chrome
This commit is contained in:
@@ -1,6 +1,26 @@
|
||||
package com.vnidrop.app
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.window.WindowDraggableArea
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.awt.awtEventOrNull
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.input.pointer.onPointerEvent
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowScope
|
||||
import androidx.compose.ui.window.application
|
||||
import com.vnidrop.app.platform.DesktopAppearanceBridge
|
||||
import com.vnidrop.app.feature.send.DesktopShareBridge
|
||||
@@ -8,11 +28,13 @@ import com.vnidrop.app.feature.receive.ExternalInvitationController
|
||||
import com.vnidrop.app.feature.receive.MaxVniDropInvitationBytes
|
||||
import com.vnidrop.app.feature.receive.VniDropInvitationExtension
|
||||
import com.vnidrop.app.feature.receive.decodeInvitationBytes
|
||||
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||
import java.awt.Desktop
|
||||
import java.io.File
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val externalInvitations = ExternalInvitationController()
|
||||
val macOs = DesktopAppearanceBridge.isMacOs()
|
||||
configureMacOsNativeAppearance()
|
||||
configureInvitationOpenHandler(externalInvitations)
|
||||
args.asSequence()
|
||||
@@ -20,7 +42,7 @@ fun main(args: Array<String>) {
|
||||
.filter { it.extension.equals(VniDropInvitationExtension, ignoreCase = true) }
|
||||
.forEach { externalInvitations.openFile(it) }
|
||||
DesktopAppearanceBridge.applyNativeAppearance = MacOsAppKitAppearance::apply
|
||||
if (System.getProperty("os.name").startsWith("Mac", ignoreCase = true)) {
|
||||
if (macOs) {
|
||||
DesktopShareBridge.shareFile = MacOsShareSheet::share
|
||||
}
|
||||
application {
|
||||
@@ -28,7 +50,16 @@ fun main(args: Array<String>) {
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "vnidrop",
|
||||
) {
|
||||
App(rememberJvmAppDependencies(externalInvitations))
|
||||
App(
|
||||
dependencies = rememberJvmAppDependencies(externalInvitations),
|
||||
windowChromeTopInset = if (macOs) MacOsTitleBarHeight else 0.dp,
|
||||
windowContentTopStartRadius = if (macOs) MacOsContentCornerRadius else 0.dp,
|
||||
windowChrome = if (macOs) {
|
||||
{ MacOsTitleBar() }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,8 +83,47 @@ private fun ExternalInvitationController.openFile(file: File) {
|
||||
}
|
||||
|
||||
private fun configureMacOsNativeAppearance() {
|
||||
if (!System.getProperty("os.name").startsWith("Mac", ignoreCase = true)) return
|
||||
if (!DesktopAppearanceBridge.isMacOs()) return
|
||||
// AWT reads this before creating the first native window. Runtime theme
|
||||
// changes are handled in the JVM platform appearance hook.
|
||||
System.setProperty("apple.awt.application.appearance", "system")
|
||||
}
|
||||
|
||||
private val MacOsTitleBarHeight = 28.dp
|
||||
private val MacOsTrafficLightsWidth = 76.dp
|
||||
private val MacOsContentCornerRadius = 20.dp
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
private fun WindowScope.MacOsTitleBar() {
|
||||
val colors = LocalVniDropColors.current
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(MacOsTitleBarHeight)
|
||||
.background(colors.backgroundSurface200),
|
||||
) {
|
||||
WindowDraggableArea(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = MacOsTrafficLightsWidth)
|
||||
.onPointerEvent(PointerEventType.Press) { event ->
|
||||
if (event.awtEventOrNull?.clickCount == 2) {
|
||||
DesktopAppearanceBridge.toggleMaximized(window)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(end = MacOsTrafficLightsWidth)) {
|
||||
BasicText(
|
||||
text = "vnidrop",
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
style = TextStyle(
|
||||
color = colors.foregroundDefault,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.vnidrop.app
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -10,6 +12,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
@@ -36,6 +39,7 @@ import com.vnidrop.app.ui.shell.ScreenScrollContainer
|
||||
import com.vnidrop.app.core.TransferDirection
|
||||
import com.vnidrop.app.ui.state.WindowClass
|
||||
import com.vnidrop.app.ui.state.windowClassFor
|
||||
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||
import com.vnidrop.app.ui.theme.VniDropTheme
|
||||
import com.vnidrop.app.ui.theme.rememberResolvedDarkTheme
|
||||
import kotlinx.coroutines.flow.filter
|
||||
@@ -43,7 +47,12 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
@Composable
|
||||
fun App(dependencies: AppDependencies) {
|
||||
fun App(
|
||||
dependencies: AppDependencies,
|
||||
windowChromeTopInset: Dp = 0.dp,
|
||||
windowContentTopStartRadius: Dp = 0.dp,
|
||||
windowChrome: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
val graphHolder = viewModel { AppGraphViewModel(dependencies) }
|
||||
val graph = graphHolder.graph
|
||||
|
||||
@@ -122,55 +131,67 @@ fun App(dependencies: AppDependencies) {
|
||||
val darkTheme = rememberResolvedDarkTheme(appState.themeMode)
|
||||
PlatformSystemAppearance(darkTheme)
|
||||
VniDropTheme(isDarkTheme = darkTheme) {
|
||||
BoxWithConstraints {
|
||||
val windowClass = windowClassFor(maxWidth.value)
|
||||
val showSendAction = appState.destination == AppDestination.Send &&
|
||||
windowClass == WindowClass.Phone &&
|
||||
sendState.selectedTransferId?.let { selectedId ->
|
||||
sendCoreState.transfers.any { it.transferId == selectedId }
|
||||
} != true &&
|
||||
sendCoreState.transfers.any { it.direction == TransferDirection.Send }
|
||||
val showReceiveAction = appState.destination == AppDestination.Receive &&
|
||||
windowClass == WindowClass.Phone &&
|
||||
!receiveState.isAcquisitionOpen &&
|
||||
receiveCoreState.transfers.any { it.direction == TransferDirection.Receive }
|
||||
AppShell(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
selectedDestination = appState.destination,
|
||||
windowClass = windowClass,
|
||||
onDestinationSelected = appViewModel::selectDestination,
|
||||
overlay = {
|
||||
VniDropSnackbarHost(graph.messages, Modifier.align(Alignment.BottomCenter))
|
||||
},
|
||||
floatingAction = if (showSendAction) {
|
||||
{
|
||||
SendFloatingAction(
|
||||
onClick = sendViewModel::openComposer,
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
|
||||
)
|
||||
}
|
||||
} else if (showReceiveAction) {
|
||||
{
|
||||
ReceiveFloatingAction(
|
||||
onClick = receiveViewModel::openAcquisition,
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(LocalVniDropColors.current.backgroundSurface200),
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = windowChromeTopInset),
|
||||
) {
|
||||
when (appState.destination) {
|
||||
AppDestination.Send -> SendRoute(sendViewModel, windowClass)
|
||||
AppDestination.Receive -> ReceiveRoute(receiveViewModel, windowClass)
|
||||
AppDestination.Settings -> ScreenScrollContainer { SettingsRoute(settingsViewModel, windowClass) }
|
||||
val windowClass = windowClassFor(maxWidth.value)
|
||||
val showSendAction = appState.destination == AppDestination.Send &&
|
||||
windowClass == WindowClass.Phone &&
|
||||
sendState.selectedTransferId?.let { selectedId ->
|
||||
sendCoreState.transfers.any { it.transferId == selectedId }
|
||||
} != true &&
|
||||
sendCoreState.transfers.any { it.direction == TransferDirection.Send }
|
||||
val showReceiveAction = appState.destination == AppDestination.Receive &&
|
||||
windowClass == WindowClass.Phone &&
|
||||
!receiveState.isAcquisitionOpen &&
|
||||
receiveCoreState.transfers.any { it.direction == TransferDirection.Receive }
|
||||
AppShell(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
selectedDestination = appState.destination,
|
||||
windowClass = windowClass,
|
||||
mainContentTopStartRadius = windowContentTopStartRadius,
|
||||
onDestinationSelected = appViewModel::selectDestination,
|
||||
overlay = {
|
||||
VniDropSnackbarHost(graph.messages, Modifier.align(Alignment.BottomCenter))
|
||||
},
|
||||
floatingAction = if (showSendAction) {
|
||||
{
|
||||
SendFloatingAction(
|
||||
onClick = sendViewModel::openComposer,
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
|
||||
)
|
||||
}
|
||||
} else if (showReceiveAction) {
|
||||
{
|
||||
ReceiveFloatingAction(
|
||||
onClick = receiveViewModel::openAcquisition,
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
) {
|
||||
when (appState.destination) {
|
||||
AppDestination.Send -> SendRoute(sendViewModel, windowClass)
|
||||
AppDestination.Receive -> ReceiveRoute(receiveViewModel, windowClass)
|
||||
AppDestination.Settings -> ScreenScrollContainer { SettingsRoute(settingsViewModel, windowClass) }
|
||||
}
|
||||
}
|
||||
ApprovalModalHost(
|
||||
state = approvalState,
|
||||
onAccept = graph.approvalCoordinator::accept,
|
||||
onRefuse = graph.approvalCoordinator::refuse,
|
||||
)
|
||||
}
|
||||
ApprovalModalHost(
|
||||
state = approvalState,
|
||||
onAccept = graph.approvalCoordinator::accept,
|
||||
onRefuse = graph.approvalCoordinator::refuse,
|
||||
)
|
||||
windowChrome?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -36,6 +37,7 @@ import org.jetbrains.compose.resources.stringResource
|
||||
fun AppSidebarNavigation(
|
||||
selected: AppDestination,
|
||||
onDestinationSelected: (AppDestination) -> Unit,
|
||||
dividerTopInset: Dp = 0.dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val colors = LocalVniDropColors.current
|
||||
@@ -62,7 +64,8 @@ fun AppSidebarNavigation(
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = dividerTopInset)
|
||||
.width(1.dp)
|
||||
.fillMaxHeight()
|
||||
.background(colors.borderDefault),
|
||||
|
||||
@@ -10,9 +10,12 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vnidrop.app.ui.navigation.AppBottomNavigation
|
||||
import com.vnidrop.app.ui.navigation.AppDestination
|
||||
@@ -26,6 +29,7 @@ fun AppShell(
|
||||
modifier: Modifier = Modifier,
|
||||
selectedDestination: AppDestination,
|
||||
windowClass: WindowClass,
|
||||
mainContentTopStartRadius: Dp = 0.dp,
|
||||
onDestinationSelected: (AppDestination) -> Unit,
|
||||
overlay: @Composable BoxScope.() -> Unit = {},
|
||||
floatingAction: (@Composable BoxScope.() -> Unit)? = null,
|
||||
@@ -49,6 +53,7 @@ fun AppShell(
|
||||
} else {
|
||||
WideShell(
|
||||
selectedDestination = selectedDestination,
|
||||
mainContentTopStartRadius = mainContentTopStartRadius,
|
||||
onDestinationSelected = onDestinationSelected,
|
||||
overlay = overlay,
|
||||
floatingAction = floatingAction,
|
||||
@@ -61,17 +66,38 @@ fun AppShell(
|
||||
@Composable
|
||||
private fun WideShell(
|
||||
selectedDestination: AppDestination,
|
||||
mainContentTopStartRadius: Dp,
|
||||
onDestinationSelected: (AppDestination) -> Unit,
|
||||
overlay: @Composable BoxScope.() -> Unit,
|
||||
floatingAction: (@Composable BoxScope.() -> Unit)?,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
val colors = LocalVniDropColors.current
|
||||
val roundedContent = mainContentTopStartRadius > 0.dp
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (roundedContent) Modifier.background(colors.backgroundSurface200) else Modifier),
|
||||
) {
|
||||
AppSidebarNavigation(
|
||||
selected = selectedDestination,
|
||||
dividerTopInset = mainContentTopStartRadius,
|
||||
onDestinationSelected = onDestinationSelected,
|
||||
)
|
||||
Box(modifier = Modifier.weight(1f).fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
if (roundedContent) {
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(topStart = mainContentTopStartRadius))
|
||||
.background(colors.backgroundDashCanvas)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
content()
|
||||
floatingAction?.invoke(this)
|
||||
Box(Modifier.fillMaxSize().padding(bottom = if (floatingAction == null) 0.dp else 72.dp)) { overlay() }
|
||||
|
||||
@@ -4,8 +4,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import java.awt.Color
|
||||
import java.awt.EventQueue
|
||||
import java.awt.Frame
|
||||
import java.awt.Window
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.JRootPane
|
||||
|
||||
@Composable
|
||||
actual fun PlatformSystemAppearance(isDarkTheme: Boolean) {
|
||||
@@ -16,10 +18,12 @@ actual fun PlatformSystemAppearance(isDarkTheme: Boolean) {
|
||||
|
||||
internal object DesktopSystemAppearance {
|
||||
private const val MAC_APPEARANCE_PROPERTY = "apple.awt.application.appearance"
|
||||
private const val FULL_WINDOW_CONTENT_PROPERTY = "apple.awt.fullWindowContent"
|
||||
private const val TRANSPARENT_TITLE_BAR_PROPERTY = "apple.awt.transparentTitleBar"
|
||||
private const val WINDOW_TITLE_VISIBLE_PROPERTY = "apple.awt.windowTitleVisible"
|
||||
|
||||
fun apply(isDarkTheme: Boolean) {
|
||||
if (!isMacOs()) return
|
||||
if (!DesktopAppearanceBridge.isMacOs()) return
|
||||
System.setProperty(MAC_APPEARANCE_PROPERTY, macOsAppearanceName(isDarkTheme))
|
||||
EventQueue.invokeLater {
|
||||
DesktopAppearanceBridge.applyNativeAppearance?.invoke(isDarkTheme)
|
||||
@@ -33,27 +37,50 @@ internal object DesktopSystemAppearance {
|
||||
if (isDarkTheme) "NSAppearanceNameDarkAqua" else "NSAppearanceNameAqua"
|
||||
|
||||
internal fun usesTransparentTitlebar(): Boolean = true
|
||||
internal fun usesFullWindowContent(): Boolean = true
|
||||
internal fun showsNativeWindowTitle(): Boolean = false
|
||||
|
||||
internal fun titlebarBackground(isDarkTheme: Boolean): Color =
|
||||
if (isDarkTheme) Color(0x12, 0x12, 0x12) else Color(0xF8, 0xF8, 0xF8)
|
||||
if (isDarkTheme) Color(0x21, 0x21, 0x21) else Color(0xF3, 0xF3, 0xF3)
|
||||
|
||||
private fun applyWindowChrome(window: Window, isDarkTheme: Boolean) {
|
||||
val background = titlebarBackground(isDarkTheme)
|
||||
window.background = background
|
||||
(window as? JFrame)?.rootPane?.let { rootPane ->
|
||||
// The titlebar stays native, but AppKit receives the resolved app
|
||||
// appearance so title text and controls switch contrast at runtime.
|
||||
rootPane.putClientProperty(TRANSPARENT_TITLE_BAR_PROPERTY, usesTransparentTitlebar())
|
||||
rootPane.background = background
|
||||
rootPane.contentPane.background = background
|
||||
}
|
||||
(window as? JFrame)?.rootPane?.let { rootPane -> applyRootPaneChrome(rootPane, background) }
|
||||
}
|
||||
|
||||
private fun isMacOs(): Boolean =
|
||||
System.getProperty("os.name").startsWith("Mac", ignoreCase = true)
|
||||
internal fun applyRootPaneChrome(rootPane: JRootPane, background: Color) {
|
||||
// Extending the Compose surface beneath the native titlebar lets the
|
||||
// window chrome and sidebar share one uninterrupted background.
|
||||
rootPane.putClientProperty(FULL_WINDOW_CONTENT_PROPERTY, usesFullWindowContent())
|
||||
rootPane.putClientProperty(TRANSPARENT_TITLE_BAR_PROPERTY, usesTransparentTitlebar())
|
||||
rootPane.putClientProperty(WINDOW_TITLE_VISIBLE_PROPERTY, showsNativeWindowTitle())
|
||||
rootPane.background = background
|
||||
rootPane.contentPane.background = background
|
||||
}
|
||||
}
|
||||
|
||||
object DesktopAppearanceBridge {
|
||||
@Volatile
|
||||
var applyNativeAppearance: ((Boolean) -> Unit)? = null
|
||||
|
||||
fun isMacOs(): Boolean = isMacOs(System.getProperty("os.name"))
|
||||
|
||||
fun toggleMaximized(window: Window) {
|
||||
if (!isMacOs()) return
|
||||
val frame = window as? Frame ?: return
|
||||
EventQueue.invokeLater {
|
||||
frame.extendedState = toggledWindowState(frame.extendedState)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isMacOs(osName: String): Boolean =
|
||||
osName.startsWith("Mac", ignoreCase = true)
|
||||
|
||||
internal fun toggledWindowState(currentState: Int): Int =
|
||||
if (currentState and Frame.MAXIMIZED_BOTH == Frame.MAXIMIZED_BOTH) {
|
||||
Frame.NORMAL
|
||||
} else {
|
||||
Frame.MAXIMIZED_BOTH
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
package com.vnidrop.app.platform
|
||||
|
||||
import java.awt.Color
|
||||
import java.awt.Frame
|
||||
import javax.swing.JRootPane
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopSystemAppearanceTest {
|
||||
@Test
|
||||
fun customWindowChromeIsMacOsOnly() {
|
||||
assertTrue(DesktopAppearanceBridge.isMacOs("Mac OS X"))
|
||||
assertFalse(DesktopAppearanceBridge.isMacOs("Windows 11"))
|
||||
assertFalse(DesktopAppearanceBridge.isMacOs("Linux"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun titlebarDoubleClickTogglesMaximizedWindowState() {
|
||||
assertEquals(Frame.MAXIMIZED_BOTH, DesktopAppearanceBridge.toggledWindowState(Frame.NORMAL))
|
||||
assertEquals(Frame.NORMAL, DesktopAppearanceBridge.toggledWindowState(Frame.MAXIMIZED_BOTH))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun macOsAppearanceNamesMatchResolvedTheme() {
|
||||
assertEquals("NSAppearanceNameDarkAqua", DesktopSystemAppearance.macOsAppearanceName(isDarkTheme = true))
|
||||
@@ -11,9 +29,9 @@ class DesktopSystemAppearanceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun titlebarBackgroundUsesLightAndDarkSurfaces() {
|
||||
assertEquals(0x121212, DesktopSystemAppearance.titlebarBackground(isDarkTheme = true).rgb and 0xFFFFFF)
|
||||
assertEquals(0xF8F8F8, DesktopSystemAppearance.titlebarBackground(isDarkTheme = false).rgb and 0xFFFFFF)
|
||||
fun titlebarBackgroundMatchesSidebarSurface() {
|
||||
assertEquals(0x212121, DesktopSystemAppearance.titlebarBackground(isDarkTheme = true).rgb and 0xFFFFFF)
|
||||
assertEquals(0xF3F3F3, DesktopSystemAppearance.titlebarBackground(isDarkTheme = false).rgb and 0xFFFFFF)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -21,6 +39,20 @@ class DesktopSystemAppearanceTest {
|
||||
assertEquals(true, DesktopSystemAppearance.usesTransparentTitlebar())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun composeContentExtendsUnderMacOsTitlebar() {
|
||||
val rootPane = JRootPane()
|
||||
val background = Color(0x21, 0x21, 0x21)
|
||||
|
||||
DesktopSystemAppearance.applyRootPaneChrome(rootPane, background)
|
||||
|
||||
assertEquals(true, rootPane.getClientProperty("apple.awt.fullWindowContent"))
|
||||
assertEquals(true, rootPane.getClientProperty("apple.awt.transparentTitleBar"))
|
||||
assertEquals(false, rootPane.getClientProperty("apple.awt.windowTitleVisible"))
|
||||
assertEquals(background, rootPane.background)
|
||||
assertEquals(background, rootPane.contentPane.background)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runtimeAppearanceCallIsFailSoft() {
|
||||
DesktopSystemAppearance.apply(isDarkTheme = true)
|
||||
|
||||
Reference in New Issue
Block a user