Skip to content

Commit 7a21448

Browse files
committed
feat(window): Add window pinning function
1 parent e59ca35 commit 7a21448

File tree

5 files changed

+161
-4
lines changed

5 files changed

+161
-4
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.jankinwu.fntv.client.icons
2+
3+
import androidx.compose.ui.graphics.Color
4+
import androidx.compose.ui.graphics.SolidColor
5+
import androidx.compose.ui.graphics.vector.ImageVector
6+
import androidx.compose.ui.graphics.vector.path
7+
import androidx.compose.ui.unit.dp
8+
9+
val Pin: ImageVector
10+
get() {
11+
if (_Pin != null) {
12+
return _Pin!!
13+
}
14+
_Pin = ImageVector.Builder(
15+
name = "Pin",
16+
defaultWidth = 200.dp,
17+
defaultHeight = 200.dp,
18+
viewportWidth = 1024f,
19+
viewportHeight = 1024f
20+
).apply {
21+
path(fill = SolidColor(Color.Black)) {
22+
moveTo(648.7f, 130.8f)
23+
arcToRelative(73.1f, 73.1f, 0f, isMoreThanHalf = false, isPositiveArc = true, 22.7f, 15.4f)
24+
lineToRelative(191.6f, 191.8f)
25+
arcToRelative(73.1f, 73.1f, 0f, isMoreThanHalf = false, isPositiveArc = true, -22.1f, 118.6f)
26+
lineToRelative(-67.9f, 30.1f)
27+
lineToRelative(-127.3f, 127.5f)
28+
lineToRelative(-10.1f, 140.2f)
29+
arcToRelative(73.1f, 73.1f, 0f, isMoreThanHalf = false, isPositiveArc = true, -124.7f, 46.4f)
30+
lineToRelative(-123.7f, -123.8f)
31+
lineToRelative(-210.7f, 211.7f)
32+
lineToRelative(-51.8f, -51.6f)
33+
lineToRelative(210.8f, -211.8f)
34+
lineToRelative(-127.9f, -128f)
35+
arcToRelative(73.1f, 73.1f, 0f, isMoreThanHalf = false, isPositiveArc = true, 46.3f, -124.6f)
36+
lineToRelative(144.2f, -10.8f)
37+
lineToRelative(125.1f, -125.2f)
38+
lineToRelative(29.4f, -67.8f)
39+
arcToRelative(73.1f, 73.1f, 0f, isMoreThanHalf = false, isPositiveArc = true, 96.2f, -38f)
40+
close()
41+
moveTo(619.6f, 197.9f)
42+
lineToRelative(-34.9f, 80.5f)
43+
lineToRelative(-154.1f, 154.3f)
44+
lineToRelative(-171.4f, 12.8f)
45+
lineToRelative(303.3f, 303.5f)
46+
lineToRelative(12f, -167.4f)
47+
lineToRelative(156.2f, -156.4f)
48+
lineToRelative(80.4f, -35.6f)
49+
lineToRelative(-191.6f, -191.7f)
50+
close()
51+
}
52+
}.build()
53+
54+
return _Pin!!
55+
}
56+
57+
@Suppress("ObjectPropertyName")
58+
private var _Pin: ImageVector? = null
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.jankinwu.fntv.client.icons
2+
3+
import androidx.compose.ui.graphics.Color
4+
import androidx.compose.ui.graphics.SolidColor
5+
import androidx.compose.ui.graphics.vector.ImageVector
6+
import androidx.compose.ui.graphics.vector.path
7+
import androidx.compose.ui.unit.dp
8+
9+
val PinFill: ImageVector
10+
get() {
11+
if (_PinFill != null) {
12+
return _PinFill!!
13+
}
14+
_PinFill = ImageVector.Builder(
15+
name = "PinFill",
16+
defaultWidth = 200.dp,
17+
defaultHeight = 200.dp,
18+
viewportWidth = 1024f,
19+
viewportHeight = 1024f
20+
).apply {
21+
path(fill = SolidColor(Color.Black)) {
22+
moveTo(648.7f, 130.8f)
23+
arcToRelative(73.1f, 73.1f, 0f, isMoreThanHalf = false, isPositiveArc = true, 22.7f, 15.4f)
24+
lineToRelative(191.6f, 191.8f)
25+
arcToRelative(73.1f, 73.1f, 0f, isMoreThanHalf = false, isPositiveArc = true, -22.1f, 118.6f)
26+
lineToRelative(-67.9f, 30.1f)
27+
lineToRelative(-127.3f, 127.5f)
28+
lineToRelative(-10.1f, 140.2f)
29+
arcToRelative(73.1f, 73.1f, 0f, isMoreThanHalf = false, isPositiveArc = true, -124.7f, 46.4f)
30+
lineToRelative(-123.7f, -123.8f)
31+
lineToRelative(-210.7f, 211.7f)
32+
lineToRelative(-51.8f, -51.6f)
33+
lineToRelative(210.8f, -211.8f)
34+
lineToRelative(-127.9f, -128f)
35+
arcToRelative(73.1f, 73.1f, 0f, isMoreThanHalf = false, isPositiveArc = true, 46.3f, -124.6f)
36+
lineToRelative(144.2f, -10.8f)
37+
lineToRelative(125.1f, -125.2f)
38+
lineToRelative(29.4f, -67.8f)
39+
arcToRelative(73.1f, 73.1f, 0f, isMoreThanHalf = false, isPositiveArc = true, 96.2f, -38f)
40+
close()
41+
}
42+
}.build()
43+
44+
return _PinFill!!
45+
}
46+
47+
@Suppress("ObjectPropertyName")
48+
private var _PinFill: ImageVector? = null

composeApp/src/jvmMain/kotlin/com/jankinwu/fntv/client/window/MacOSWindowFrame.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import androidx.compose.ui.unit.dp
3333
import androidx.compose.ui.window.FrameWindowScope
3434
import androidx.compose.ui.window.WindowPlacement
3535
import androidx.compose.ui.window.WindowState
36+
import com.jankinwu.fntv.client.icons.Pin
37+
import com.jankinwu.fntv.client.icons.PinFill
3638
import com.jankinwu.fntv.client.icons.RefreshCircle
3739
import com.jankinwu.fntv.client.ui.component.common.HasNewVersionTag
3840
import com.jankinwu.fntv.client.ui.providable.LocalPlayerManager
@@ -54,6 +56,8 @@ fun FrameWindowScope.MacOSWindowFrame(
5456
icon: Painter?,
5557
captionBarHeight: Dp,
5658
onBackButtonClick: () -> Unit,
59+
isAlwaysOnTop: Boolean = false,
60+
onToggleAlwaysOnTop: () -> Unit = {},
5761
onRefreshClick: (() -> Unit)? = null,
5862
content: @Composable (windowInset: WindowInsets, captionBarInset: WindowInsets) -> Unit
5963
) {
@@ -127,6 +131,15 @@ fun FrameWindowScope.MacOSWindowFrame(
127131
)
128132
}
129133

134+
Icon(
135+
imageVector = if (isAlwaysOnTop) PinFill else Pin,
136+
contentDescription = "Always On Top",
137+
modifier = Modifier
138+
.padding(start = 6.dp)
139+
.size(16.dp)
140+
.clickable { onToggleAlwaysOnTop() }
141+
)
142+
130143
// 添加刷新按钮
131144
if (onRefreshClick != null) {
132145
val rotation = remember { Animatable(0f) }

composeApp/src/jvmMain/kotlin/com/jankinwu/fntv/client/window/WindowFrame.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.jankinwu.fntv.client.window
22

33
import androidx.compose.foundation.layout.WindowInsets
44
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.LaunchedEffect
56
import androidx.compose.runtime.getValue
67
import androidx.compose.runtime.mutableStateOf
78
import androidx.compose.runtime.remember
@@ -34,6 +35,12 @@ fun FrameWindowScope.WindowFrame(
3435
val supportBackdrop = hostOs.isWindows && isWindows11OrLater()
3536
val refreshManager = remember { RefreshManager() }
3637
var isRefreshing by remember { mutableStateOf(false) }
38+
var isAlwaysOnTop by remember { mutableStateOf(false) }
39+
40+
LaunchedEffect(isAlwaysOnTop) {
41+
window.isAlwaysOnTop = isAlwaysOnTop
42+
}
43+
3744
AppTheme(
3845
!supportBackdrop,
3946
state,
@@ -52,6 +59,8 @@ fun FrameWindowScope.WindowFrame(
5259
backButtonVisible = backButtonVisible && !isCollapsed,
5360
backButtonEnabled = backButtonEnabled,
5461
backButtonClick = backButtonClick,
62+
isAlwaysOnTop = isAlwaysOnTop,
63+
onToggleAlwaysOnTop = { isAlwaysOnTop = !isAlwaysOnTop },
5564
onRefreshClick = {
5665
// 执行刷新操作
5766
refreshManager.requestRefresh {
@@ -79,6 +88,8 @@ fun FrameWindowScope.WindowFrame(
7988
icon = if (isCollapsed) null else icon,
8089
title = if (isCollapsed) "" else title,
8190
state = state,
91+
isAlwaysOnTop = isAlwaysOnTop,
92+
onToggleAlwaysOnTop = { isAlwaysOnTop = !isAlwaysOnTop },
8293
onRefreshClick = {
8394
// 执行刷新操作
8495
refreshManager.requestRefresh {

composeApp/src/jvmMain/kotlin/com/jankinwu/fntv/client/window/WindowsWindowFrame.kt

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import androidx.compose.ui.window.FrameWindowScope
5555
import androidx.compose.ui.window.WindowPlacement
5656
import androidx.compose.ui.window.WindowState
5757
import androidx.compose.ui.zIndex
58+
import com.jankinwu.fntv.client.icons.Pin as PinIcon
59+
import com.jankinwu.fntv.client.icons.PinFill as PinFillIcon
5860
import com.jankinwu.fntv.client.icons.RefreshCircle
5961
import com.jankinwu.fntv.client.jna.windows.ComposeWindowProcedure
6062
import com.jankinwu.fntv.client.ui.component.common.HasNewVersionTag
@@ -103,6 +105,8 @@ fun FrameWindowScope.WindowsWindowFrame(
103105
backButtonVisible: Boolean = true,
104106
backButtonEnabled: Boolean = false,
105107
backButtonClick: () -> Unit = {},
108+
isAlwaysOnTop: Boolean = false,
109+
onToggleAlwaysOnTop: () -> Unit = {},
106110
onRefreshClick: (() -> Unit)? = null,
107111
onRefreshAnimationStart: (() -> Unit)? = null,
108112
onRefreshAnimationEnd: (() -> Unit)? = null,
@@ -231,6 +235,8 @@ fun FrameWindowScope.WindowsWindowFrame(
231235
windowHandle = procedure.windowHandle,
232236
isMaximize = state.placement == WindowPlacement.Maximized,
233237
onCloseRequest = onCloseRequest,
238+
isAlwaysOnTop = isAlwaysOnTop,
239+
onToggleAlwaysOnTop = onToggleAlwaysOnTop,
234240
onRefreshClick = onRefreshClick,
235241
onRefreshAnimationStart = onRefreshAnimationStart,
236242
onRefreshAnimationEnd = onRefreshAnimationEnd,
@@ -265,6 +271,8 @@ fun Window.CaptionButtonRow(
265271
frameColorEnabled: Boolean,
266272
onCloseRequest: () -> Unit,
267273
modifier: Modifier = Modifier,
274+
isAlwaysOnTop: Boolean = false,
275+
onToggleAlwaysOnTop: () -> Unit = {},
268276
onRefreshClick: (() -> Unit)? = null,
269277
onRefreshAnimationStart: (() -> Unit)? = null,
270278
onRefreshAnimationEnd: (() -> Unit)? = null,
@@ -313,6 +321,13 @@ fun Window.CaptionButtonRow(
313321
)
314322
}
315323
}
324+
CaptionButton(
325+
onClick = onToggleAlwaysOnTop,
326+
icon = if (isAlwaysOnTop) CaptionButtonIcon.Unpin else CaptionButtonIcon.Pin,
327+
isActive = isActive,
328+
colors = colors,
329+
iconSize = 15.dp
330+
)
316331
CaptionButton(
317332
onClick = {
318333
User32.INSTANCE.ShowWindow(windowHandle, WinUser.SW_MINIMIZE)
@@ -370,7 +385,8 @@ fun CaptionButton(
370385
modifier: Modifier = Modifier,
371386
colors: VisualStateScheme<CaptionButtonColor> = CaptionButtonDefaults.defaultColors(),
372387
interaction: MutableInteractionSource = remember { MutableInteractionSource() },
373-
rotation: Float = 0f
388+
rotation: Float = 0f,
389+
iconSize: Dp = 13.dp
374390
) {
375391
val color = colors.schemeFor(interaction.collectVisualState(false))
376392
TooltipBox(
@@ -397,7 +413,7 @@ fun CaptionButton(
397413
shape = RectangleShape
398414
) {
399415
val fontFamily by rememberFontIconFamily()
400-
if (fontFamily != null) {
416+
if (fontFamily != null && !icon.useImageVector) {
401417
Text(
402418
text = icon.glyph.toString(),
403419
fontFamily = fontFamily,
@@ -412,7 +428,7 @@ fun CaptionButton(
412428
Icon(
413429
imageVector = icon.imageVector,
414430
contentDescription = null,
415-
modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center).size(13.dp)
431+
modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center).size(iconSize)
416432
.graphicsLayer { // 应用旋转
417433
rotationZ = rotation
418434
}
@@ -525,7 +541,8 @@ data class CaptionButtonColor(
525541

526542
enum class CaptionButtonIcon(
527543
val glyph: Char,
528-
val imageVector: ImageVector
544+
val imageVector: ImageVector,
545+
val useImageVector: Boolean = false
529546
) {
530547
Minimize(
531548
glyph = '\uE921',
@@ -547,6 +564,16 @@ enum class CaptionButtonIcon(
547564
glyph = '\uE72C',
548565
imageVector = RefreshCircle
549566
),
567+
Pin(
568+
glyph = '\uE718',
569+
imageVector = PinIcon,
570+
useImageVector = true
571+
),
572+
Unpin(
573+
glyph = '\uE840',
574+
imageVector = PinFillIcon,
575+
useImageVector = true
576+
),
550577
}
551578

552579
fun Rect.contains(x: Float, y: Float): Boolean {

0 commit comments

Comments
 (0)