Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support windows transparent title bar. #1578

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

Sanlorng
Copy link

  1. Add windows transparent title bar.
  2. Move macOS special window adaption to MacOSWindowFrame.kt.
  3. Refactor WindowsWindowUtils.kt partially.
output20250125.mp4

1. Add windows transparent title bar.
2. Move macOS special window adaption to MacOSWindowFrame.kt.
3. Refactor WindowsWindowUtils.kt partially.
}
Box(modifier = Modifier.fillMaxSize()) {
val isFullScreen = isSystemInFullscreen()
val isTitleBarVisible = remember(isFullScreen) { mutableStateOf(!isFullScreen) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remember { derivedStateOf { !isFullScreen } }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

只是取否无需有这个 remember/state

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isTitleBarVisible初始状态需要在isFullScreen变化时重新初始化,后续还有控制它在全屏下显示的需求,你往下看有一个Spacer,设置了一个hoverable,用来控制TitleBar显示。

@OptIn(InternalComposeUiApi::class)
val classLoader = ComposeScene::class.java.classLoader!!

private val rootNodeOwnerOwnerField =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这些 class 应该缓存

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这些 class 应该缓存

ComposeScene只负责拿ClassLoader,不需要缓存

Copy link
Member

@StageGuard StageGuard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

除了 review 的问题,还需要解决以下问题

  1. 窗口只有 left right 和 bottom 有边框线,top 没有

image
image

  1. 拖拽调整大小的光标似乎只有在窗口内才会检测状态,在窗口外会显示错误的拖拽光标

https://t.me/sg_pubmsg_backup/3

  1. 最大化时全屏的窗口动画有问题,似乎先移动到了最大化前小窗口的位置再瞬移。

https://t.me/sg_pubmsg_backup/2

  1. 反射的类需要添加 progurad 规则


private val layers =
CopiedList {
for (layer in layersRef) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 CopiedList 有啥用,为什么不直接在下面的 hitTest 里 layersRef.forEach

Copy link
Member

@Him188 Him188 Jan 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CopiedList 的实现比较晦涩, 我也建议考虑 forEach

fun windowIsActive(platformWindow: PlatformWindow): Flow<Boolean?> {
return snapshotFlow { platformWindow.titleBarWindowProc }
.flatMapConcat {
it?.windowIsActive ?: flow<Boolean?> { emit(null) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default to MutableStateFlow<Boolean?>(false)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSF changes semantics.

use flowOf(null) instaed

Comment on lines 432 to 438
LaunchedEffect(themeSettings.darkMode, titleBarThemeController, systemTheme) {
titleBarThemeController?.isDark = when (themeSettings.darkMode) {
DarkMode.AUTO -> systemTheme == SystemTheme.Dark
DarkMode.LIGHT -> false
DarkMode.DARK -> true
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to use LaunchedEffect

Suggested change
LaunchedEffect(themeSettings.darkMode, titleBarThemeController, systemTheme) {
titleBarThemeController?.isDark = when (themeSettings.darkMode) {
DarkMode.AUTO -> systemTheme == SystemTheme.Dark
DarkMode.LIGHT -> false
DarkMode.DARK -> true
}
}
titleBarThemeController?.isDark = when (themeSettings.darkMode) {
DarkMode.AUTO -> systemTheme == SystemTheme.Dark
DarkMode.LIGHT -> false
DarkMode.DARK -> true
}


@OptIn(ExperimentalTextApi::class, InternalComposeUiApi::class)
@Composable
fun FrameWindowScope.WindowsWindowFrame(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个有可能能写 ui tset 吗

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Window proc和LayoutHitTestOwner需要有Window显示才能测试

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

那 headless 模式确实应该是不行的,那就算了

Comment on lines +33 to +49
fun rememberLayoutHitTestOwner(): LayoutHitTestOwner? {
val scene = LocalComposeScene.current ?: return null
return remember(scene) {
when (scene::class.qualifiedName) {
"androidx.compose.ui.scene.CanvasLayersComposeSceneImpl" -> {
CanvasLayersLayoutHitTestOwner(scene)
}

"androidx.compose.ui.scene.PlatformLayersComposeSceneImpl" -> {
PlatformLayersLayoutHitTestOwner(scene)
}

else -> error("unsupported compose scene")
}
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need proguard rules


private val layers =
CopiedList {
for (layer in layersRef) {
Copy link
Member

@Him188 Him188 Jan 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CopiedList 的实现比较晦涩, 我也建议考虑 forEach

//This controller should only be created and passed when the caption button is in the top end position.
val LocalTitleBarThemeController = compositionLocalOf<TitleBarThemeController?> { null }

class TitleBarThemeController {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work on Android/mac?

如果不是, 需要注释

Comment on lines 304 to 312
val hitClient: Int get() = HTCLIENT

val hitMaxButton: Int get() = HTMAXBUTTON

val hitMinimize: Int get() = HTMINBUTTON

val hitClose: Int get() = HTCLOSE

val hitCaption: Int get() = HTCAPTION
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to be public?

Copy link
Author

@Sanlorng Sanlorng Jan 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to be public?

ExtendUser32和WindowsWindowFrame不在同一个模块,如果不需要这样的话,我用suppress invisible member来处理吧

Comment on lines 18 to 20
val titleBarController = LocalTitleBarThemeController.current
DisposableEffect(titleBarController) {
if (titleBarController == null) return@DisposableEffect onDispose { }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val titleBarController = LocalTitleBarThemeController.current
DisposableEffect(titleBarController) {
if (titleBarController == null) return@DisposableEffect onDispose { }
val titleBarController = LocalTitleBarThemeController.current ?: return
DisposableEffect(titleBarController) {

Comment on lines 177 to 178
val isRightCaptionButton =
WindowInsets.desktopCaptionButton.getRight(LocalDensity.current, LocalLayoutDirection.current) > 0
Copy link
Member

@Him188 Him188 Jan 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个可以写一个通用函数, 因为以后很可能还会有这些逻辑
期望调用方法:

WindowInsets.desktopCaptionButton.isTopRight()

Comment on lines +181 to 183
Modifier.ifThen(navigationLayoutType != NavigationSuiteType.NavigationBar && !isRightCaptionButton) {
// macos 标题栏只会在 NavigationRail 的区域内, TabContent 区域无需这些 padding.
consumeWindowInsets(WindowInsets.desktopTitleBar())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可以优化一下注释

Suggested change
Modifier.ifThen(navigationLayoutType != NavigationSuiteType.NavigationBar && !isRightCaptionButton) {
// macos 标题栏只会在 NavigationRail 的区域内, TabContent 区域无需这些 padding.
consumeWindowInsets(WindowInsets.desktopTitleBar())
Modifier.ifThen(
navigationLayoutType != NavigationSuiteType.NavigationBar
&& !isRightCaptionButton // Windows caption button 在右侧, 没有足够空间放置按钮, 需要保留 title bar insets
) {
// macos 标题栏只会在 NavigationRail 的区域内, TabContent 区域无需这些 padding.
consumeWindowInsets(WindowInsets.desktopTitleBar())

) {
val layoutHitTestOwner = rememberLayoutHitTestOwner()
val platformWindow = LocalPlatformWindow.current
if (layoutHitTestOwner != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to write:

if (layoutHitTestOwner == null) {
    content()
    return
}

// rest of code. 

这样逻辑更简单, 因为提前 return 掉了, 就不需要看后面代码了

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fail fast 减少嵌套

@Him188
Copy link
Member

Him188 commented Jan 25, 2025

即使我一直参与群内讨论, 我也仍然不是很能了解现在 insets 架构, 对于以后新开发者来说就更不好理解了. 所以最好要有一个 insets 系统的描述性文档, 也就是:

  • 现在的 insets 系统有哪些组件? WindowInsets.desktopTitleBar(), WindowInsets.captionButton, LocalCaptionButtonInsets 什么的有什么区别/关联?
  • 每个页面是否默认会有这些 insets/padding
  • 处理这些 insets 的示例

The `LaunchedEffect` used to update the title bar theme based on dark mode settings and system theme has been removed. The title bar theme is now updated directly, eliminating the need for a coroutine scope.
Simplify the `windowIsActive` and `windowAccentColor` flows in `WindowsWindowUtils` by using `flowOf` instead of `flow` for emitting null or default values. This makes the code more concise and readable.
This commit refactors the code to use the `isTopRight()` function for checking if window insets are on the top right.

This simplifies the logic and makes the code more readable.
The change handles the case when titleBarController is null in DarkCaptionButtonAppearance by returning early.
@StageGuard
Copy link
Member

关闭窗口必报错

Jan 26, 2025 11:11:38 AM com.sun.jna.Native$1 uncaughtException
WARNING: JNA: Callback me.him188.ani.app.platform.window.TitleBarWindowProc@1fdbebff threw the following exception
java.lang.IllegalStateException: Can't lock DrawingSurface
	at org.jetbrains.skiko.DrawingSurface.lock(AWT.kt:35)
	at org.jetbrains.skiko.AWTKt.useDrawingSurfaceInfo(AWT.kt:94)
	at org.jetbrains.skiko.AWTKt.useDrawingSurfacePlatformInfo(AWT.kt:12)
	at org.jetbrains.skiko.HardwareLayer.getContentHandle(HardwareLayer.kt:41)
	at org.jetbrains.skiko.SkiaLayer.getContentHandle(SkiaLayer.awt.kt:226)
	at me.him188.ani.app.platform.window.TitleBarWindowProc.callback(WindowsWindowUtils.kt:838)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at com.sun.jna.CallbackReference$DefaultCallbackProxy.invokeCallback(CallbackReference.java:585)
	at com.sun.jna.CallbackReference$DefaultCallbackProxy.callback(CallbackReference.java:616)
	at java.desktop/sun.awt.windows.WToolkit.eventLoop(Native Method)
	at java.desktop/sun.awt.windows.WToolkit.run(WToolkit.java:360)
	at java.base/java.lang.Thread.run(Thread.java:1583)

Sanlorng and others added 8 commits January 26, 2025 12:52
Update the desktop hit test implementation to use the `_layersCopyCache` field instead of the `layers` field. This ensures that the hit test is performed on the correct set of layers and improves performance.
This change avoids title bar buttons hit test interference by excluding AbstractClickableNodes and HoverableNodes from hit test results. This ensures that pointer input for Material 3 components is detected correctly.
The change handles the case when layoutHitTestOwner is null in WindowsWindowFrame by returning early and calling the content function.
This change improves the organization of the code and makes it more maintainable.
the `SkiaLayerHitTestWindowProc` class is now internal and its `contentHandle` property is exposed for access.
Move the `windowsBuildNumber()` function to `WindowsWindowUtils` to avoid redundant code and improve maintainability.
Also draw a fake top border when window is inactive on Windows version less than 22000 (Windows 11).
@Him188
Copy link
Member

Him188 commented Jan 26, 2025

解决 conversation 后可以 request 一下我

…less than 22000 (Windows 11)

This commit fixes the issue where the top border of the window is not drawn correctly when the window is inactive on Windows versions less than 22000 (Windows 11).
It achieves this by drawing a fake top border using `drawLine` when the window is inactive and the Windows version is less than 22000.
The color of the fake border is determined based on whether the window frame is colorful and the window's active state.
If the window frame is colorful and the window is active, the accent color is used.
If the window frame is not colorful or the window is inactive, a default color is used.
The top padding calculation for maximized windows on Windows has been corrected. Previously, it was not taking into account the frameX value, which could result in incorrect padding.

This change ensures that the top padding is calculated correctly, even when the window is maximized.
The `hitTest` logic in `WindowsWindowUtils` has been updated to improve window resizing behavior.
- Add `hitTestWindowResizerBorder` method to determine if the cursor is within the window resizer border.
- Call `hitTestWindowResizerBorder` before `childHitTest` and only call it if the window is not maximized.
- Update `WM_NCHITTEST` message logic.
- Return `HTNOWHERE` when `hitTestWindowResizerBorder` is not triggered.
- `hitTestResult` will use the result of `callResult.toInt()` from `CallWindowProc` in some cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants