mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(desktop): polish Linux native experience
This commit is contained in:
@@ -9,13 +9,13 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.window.WindowDraggableArea
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -53,10 +53,12 @@ import com.vnidrop.app.feature.receive.VniDropInvitationExtension
|
||||
import com.vnidrop.app.feature.receive.decodeInvitationBytes
|
||||
import com.vnidrop.app.platform.DesktopAppearanceBridge
|
||||
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||
import io.github.vinceglb.filekit.FileKit
|
||||
import java.awt.Desktop
|
||||
import java.io.File
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
FileKit.init(appId = "vnidrop")
|
||||
val externalInvitations = ExternalInvitationController()
|
||||
val linux = DesktopAppearanceBridge.isLinux()
|
||||
configureInvitationOpenHandler(externalInvitations)
|
||||
@@ -114,9 +116,10 @@ private fun ExternalInvitationController.openFile(file: File) {
|
||||
}
|
||||
}
|
||||
|
||||
private val LinuxTitleBarHeight = 40.dp
|
||||
private val LinuxWindowControlWidth = 46.dp
|
||||
private val LinuxWindowControlsWidth = 138.dp
|
||||
private val LinuxTitleBarHeight = 48.dp
|
||||
private val LinuxWindowControlHitTargetSize = 34.dp
|
||||
private val LinuxWindowControlVisualSize = 28.dp
|
||||
private val LinuxWindowControlsWidth = 120.dp
|
||||
private val DesktopContentCornerRadius = 20.dp
|
||||
|
||||
@Composable
|
||||
@@ -156,7 +159,9 @@ private fun WindowScope.LinuxTitleBar(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.fillMaxHeight(),
|
||||
.padding(end = 10.dp)
|
||||
.background(colors.backgroundSurface300, RoundedCornerShape(20.dp))
|
||||
.padding(3.dp),
|
||||
) {
|
||||
LinuxWindowControlButton(
|
||||
icon = LinuxMinimizeIcon,
|
||||
@@ -189,17 +194,15 @@ private fun LinuxWindowControlButton(
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val hovered by interactionSource.collectIsHoveredAsState()
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
val active = hovered || pressed
|
||||
val background = when {
|
||||
isClose && active -> colors.destructiveDefault
|
||||
active -> colors.backgroundOverlayHover
|
||||
else -> Color.Transparent
|
||||
val visualState = linuxWindowControlVisualState(isClose, hovered, pressed)
|
||||
val background = when (visualState) {
|
||||
LinuxWindowControlVisualState.Default -> Color.Transparent
|
||||
LinuxWindowControlVisualState.NeutralActive -> colors.backgroundOverlayHover
|
||||
LinuxWindowControlVisualState.DestructiveActive -> colors.destructiveDefault
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(LinuxWindowControlWidth)
|
||||
.fillMaxHeight()
|
||||
.background(background)
|
||||
.size(LinuxWindowControlHitTargetSize)
|
||||
.hoverable(interactionSource)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
@@ -209,15 +212,41 @@ private fun LinuxWindowControlButton(
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
painter = rememberVectorPainter(icon),
|
||||
contentDescription = contentDescription,
|
||||
colorFilter = ColorFilter.tint(if (isClose && active) Color.White else colors.foregroundLight),
|
||||
modifier = Modifier.size(15.dp),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(LinuxWindowControlVisualSize)
|
||||
.background(background, CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
painter = rememberVectorPainter(icon),
|
||||
contentDescription = contentDescription,
|
||||
colorFilter = ColorFilter.tint(
|
||||
if (visualState == LinuxWindowControlVisualState.DestructiveActive) Color.White
|
||||
else colors.foregroundLight,
|
||||
),
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class LinuxWindowControlVisualState {
|
||||
Default,
|
||||
NeutralActive,
|
||||
DestructiveActive,
|
||||
}
|
||||
|
||||
internal fun linuxWindowControlVisualState(
|
||||
isClose: Boolean,
|
||||
isHovered: Boolean,
|
||||
isPressed: Boolean,
|
||||
): LinuxWindowControlVisualState = when {
|
||||
isClose && (isHovered || isPressed) -> LinuxWindowControlVisualState.DestructiveActive
|
||||
isHovered || isPressed -> LinuxWindowControlVisualState.NeutralActive
|
||||
else -> LinuxWindowControlVisualState.Default
|
||||
}
|
||||
|
||||
internal fun toggledWindowPlacement(current: WindowPlacement): WindowPlacement =
|
||||
if (current == WindowPlacement.Maximized) WindowPlacement.Floating else WindowPlacement.Maximized
|
||||
|
||||
|
||||
@@ -10,4 +10,28 @@ class DesktopWindowChromeTest {
|
||||
assertEquals(WindowPlacement.Maximized, toggledWindowPlacement(WindowPlacement.Floating))
|
||||
assertEquals(WindowPlacement.Floating, toggledWindowPlacement(WindowPlacement.Maximized))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun closeControlUsesDestructiveStateWhenHoveredOrPressed() {
|
||||
assertEquals(
|
||||
LinuxWindowControlVisualState.DestructiveActive,
|
||||
linuxWindowControlVisualState(isClose = true, isHovered = true, isPressed = false),
|
||||
)
|
||||
assertEquals(
|
||||
LinuxWindowControlVisualState.DestructiveActive,
|
||||
linuxWindowControlVisualState(isClose = true, isHovered = false, isPressed = true),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun standardControlsKeepNeutralInteractionState() {
|
||||
assertEquals(
|
||||
LinuxWindowControlVisualState.NeutralActive,
|
||||
linuxWindowControlVisualState(isClose = false, isHovered = true, isPressed = false),
|
||||
)
|
||||
assertEquals(
|
||||
LinuxWindowControlVisualState.Default,
|
||||
linuxWindowControlVisualState(isClose = false, isHovered = false, isPressed = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user