-
Notifications
You must be signed in to change notification settings - Fork 93
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
base: main
Are you sure you want to change the base?
Conversation
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) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remember { derivedStateOf { !isFullScreen } }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
只是取否无需有这个 remember/state
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这些 class 应该缓存
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这些 class 应该缓存
ComposeScene只负责拿ClassLoader,不需要缓存
app/shared/ui-foundation/src/desktopMain/kotlin/platform/window/TitleBarThemeController.kt
Show resolved
Hide resolved
Cache the `HitTestResult` constructor to avoid reflection overhead on every hit test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
除了 review 的问题,还需要解决以下问题
- 窗口只有 left right 和 bottom 有边框线,top 没有
- 拖拽调整大小的光标似乎只有在窗口内才会检测状态,在窗口外会显示错误的拖拽光标
https://t.me/sg_pubmsg_backup/3
- 最大化时全屏的窗口动画有问题,似乎先移动到了最大化前小窗口的位置再瞬移。
https://t.me/sg_pubmsg_backup/2
- 反射的类需要添加 progurad 规则
app/shared/ui-foundation/src/desktopMain/kotlin/platform/window/LayoutHitTestOwner.kt
Outdated
Show resolved
Hide resolved
|
||
private val layers = | ||
CopiedList { | ||
for (layer in layersRef) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个 CopiedList 有啥用,为什么不直接在下面的 hitTest 里 layersRef.forEach
There was a problem hiding this comment.
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) } |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
LaunchedEffect(themeSettings.darkMode, titleBarThemeController, systemTheme) { | ||
titleBarThemeController?.isDark = when (themeSettings.darkMode) { | ||
DarkMode.AUTO -> systemTheme == SystemTheme.Dark | ||
DarkMode.LIGHT -> false | ||
DarkMode.DARK -> true | ||
} | ||
} |
There was a problem hiding this comment.
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
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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个有可能能写 ui tset 吗
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Window proc和LayoutHitTestOwner需要有Window显示才能测试
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
那 headless 模式确实应该是不行的,那就算了
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") | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
如果不是, 需要注释
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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来处理吧
val titleBarController = LocalTitleBarThemeController.current | ||
DisposableEffect(titleBarController) { | ||
if (titleBarController == null) return@DisposableEffect onDispose { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
val titleBarController = LocalTitleBarThemeController.current | |
DisposableEffect(titleBarController) { | |
if (titleBarController == null) return@DisposableEffect onDispose { } | |
val titleBarController = LocalTitleBarThemeController.current ?: return | |
DisposableEffect(titleBarController) { |
val isRightCaptionButton = | ||
WindowInsets.desktopCaptionButton.getRight(LocalDensity.current, LocalLayoutDirection.current) > 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个可以写一个通用函数, 因为以后很可能还会有这些逻辑
期望调用方法:
WindowInsets.desktopCaptionButton.isTopRight()
Modifier.ifThen(navigationLayoutType != NavigationSuiteType.NavigationBar && !isRightCaptionButton) { | ||
// macos 标题栏只会在 NavigationRail 的区域内, TabContent 区域无需这些 padding. | ||
consumeWindowInsets(WindowInsets.desktopTitleBar()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
可以优化一下注释
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) { |
There was a problem hiding this comment.
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 掉了, 就不需要看后面代码了
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fail fast 减少嵌套
即使我一直参与群内讨论, 我也仍然不是很能了解现在 insets 架构, 对于以后新开发者来说就更不好理解了. 所以最好要有一个 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.
关闭窗口必报错
|
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).
解决 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.
output20250125.mp4