mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(ui): support native desktop backdrops
This commit is contained in:
@@ -13,6 +13,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.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -54,6 +55,8 @@ fun App(
|
||||
dependencies: AppDependencies,
|
||||
windowChromeTopInset: Dp = 0.dp,
|
||||
windowContentTopStartRadius: Dp = 0.dp,
|
||||
useNativeWindowBackdrop: Boolean = false,
|
||||
onResolvedDarkThemeChanged: (Boolean) -> Unit = {},
|
||||
windowChrome: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
val graphHolder = viewModel { AppGraphViewModel(dependencies) }
|
||||
@@ -140,13 +143,19 @@ fun App(
|
||||
}
|
||||
|
||||
val darkTheme = rememberResolvedDarkTheme(appState.themeMode)
|
||||
LaunchedEffect(darkTheme, onResolvedDarkThemeChanged) {
|
||||
onResolvedDarkThemeChanged(darkTheme)
|
||||
}
|
||||
PlatformSystemAppearance(darkTheme)
|
||||
CompositionLocalProvider(LocalUiPlatform provides dependencies.environment.uiPlatform) {
|
||||
VniDropTheme(isDarkTheme = darkTheme) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(LocalVniDropColors.current.backgroundSurface200),
|
||||
.background(
|
||||
if (useNativeWindowBackdrop) Color.Transparent
|
||||
else LocalVniDropColors.current.backgroundSurface200,
|
||||
),
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
@@ -171,6 +180,7 @@ fun App(
|
||||
windowClass = windowClass,
|
||||
uiPlatform = dependencies.environment.uiPlatform,
|
||||
mainContentTopStartRadius = windowContentTopStartRadius,
|
||||
useNativeWindowBackdrop = useNativeWindowBackdrop,
|
||||
onDestinationSelected = appViewModel::selectDestination,
|
||||
overlay = {
|
||||
VniDropSnackbarHost(graph.messages, Modifier.align(Alignment.BottomCenter))
|
||||
|
||||
@@ -55,6 +55,7 @@ fun AppSidebarNavigation(
|
||||
style: NavigationStyle,
|
||||
onDestinationSelected: (AppDestination) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
useNativeWindowBackdrop: Boolean = false,
|
||||
) {
|
||||
when (style) {
|
||||
NavigationStyle.AndroidRail -> AndroidNavigationRail(selected, onDestinationSelected, modifier)
|
||||
@@ -62,6 +63,7 @@ fun AppSidebarNavigation(
|
||||
selected = selected,
|
||||
onDestinationSelected = onDestinationSelected,
|
||||
modifier = modifier,
|
||||
useNativeWindowBackdrop = useNativeWindowBackdrop,
|
||||
)
|
||||
NavigationStyle.AndroidBottomBar -> error("Bottom navigation is rendered by the phone shell")
|
||||
}
|
||||
@@ -103,13 +105,14 @@ private fun DesktopSidebarNavigation(
|
||||
selected: AppDestination,
|
||||
onDestinationSelected: (AppDestination) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
useNativeWindowBackdrop: Boolean = false,
|
||||
) {
|
||||
val colors = LocalVniDropColors.current
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(DesktopNavigationWidthDp.dp)
|
||||
.fillMaxHeight()
|
||||
.background(colors.backgroundSurface200)
|
||||
.background(if (useNativeWindowBackdrop) Color.Transparent else colors.backgroundSurface200)
|
||||
.padding(horizontal = 12.dp, vertical = 14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
|
||||
@@ -15,6 +15,7 @@ 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.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vnidrop.app.UiPlatform
|
||||
@@ -33,6 +34,7 @@ fun AppShell(
|
||||
windowClass: WindowClass,
|
||||
uiPlatform: UiPlatform,
|
||||
mainContentTopStartRadius: Dp = 0.dp,
|
||||
useNativeWindowBackdrop: Boolean = false,
|
||||
onDestinationSelected: (AppDestination) -> Unit,
|
||||
overlay: @Composable BoxScope.() -> Unit = {},
|
||||
floatingAction: (@Composable BoxScope.() -> Unit)? = null,
|
||||
@@ -40,11 +42,13 @@ fun AppShell(
|
||||
) {
|
||||
val colors = LocalVniDropColors.current
|
||||
val navigationStyle = navigationStyleFor(uiPlatform, windowClass)
|
||||
val windowSurface = if (useNativeWindowBackdrop) Color.Transparent else colors.backgroundDashCanvas
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(colors.backgroundDashCanvas),
|
||||
color = colors.backgroundDashCanvas,
|
||||
.background(windowSurface),
|
||||
color = windowSurface,
|
||||
contentColor = colors.foregroundDefault,
|
||||
) {
|
||||
if (navigationStyle == NavigationStyle.AndroidBottomBar) {
|
||||
PhoneShell(
|
||||
@@ -59,6 +63,7 @@ fun AppShell(
|
||||
selectedDestination = selectedDestination,
|
||||
navigationStyle = navigationStyle,
|
||||
mainContentTopStartRadius = mainContentTopStartRadius,
|
||||
useNativeWindowBackdrop = useNativeWindowBackdrop,
|
||||
onDestinationSelected = onDestinationSelected,
|
||||
overlay = overlay,
|
||||
floatingAction = floatingAction,
|
||||
@@ -73,6 +78,7 @@ private fun WideShell(
|
||||
selectedDestination: AppDestination,
|
||||
navigationStyle: NavigationStyle,
|
||||
mainContentTopStartRadius: Dp,
|
||||
useNativeWindowBackdrop: Boolean,
|
||||
onDestinationSelected: (AppDestination) -> Unit,
|
||||
overlay: @Composable BoxScope.() -> Unit,
|
||||
floatingAction: (@Composable BoxScope.() -> Unit)?,
|
||||
@@ -83,11 +89,18 @@ private fun WideShell(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (roundedContent) Modifier.background(colors.backgroundSurface200) else Modifier),
|
||||
.then(
|
||||
if (roundedContent && !useNativeWindowBackdrop) {
|
||||
Modifier.background(colors.backgroundSurface200)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
AppSidebarNavigation(
|
||||
selected = selectedDestination,
|
||||
style = navigationStyle,
|
||||
useNativeWindowBackdrop = useNativeWindowBackdrop,
|
||||
onDestinationSelected = onDestinationSelected,
|
||||
)
|
||||
Box(
|
||||
@@ -96,13 +109,12 @@ private fun WideShell(
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
if (roundedContent) {
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(topStart = mainContentTopStartRadius))
|
||||
.background(colors.backgroundDashCanvas)
|
||||
Modifier.clip(RoundedCornerShape(topStart = mainContentTopStartRadius))
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
)
|
||||
.background(colors.backgroundDashCanvas),
|
||||
) {
|
||||
content()
|
||||
floatingAction?.invoke(this)
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -18,7 +19,12 @@ import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.captureToImage
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.toPixelMap
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.test.v2.runComposeUiTest
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -55,6 +61,7 @@ import com.vnidrop.app.ui.navigation.AppDestination
|
||||
import com.vnidrop.app.ui.platform.LocalUiPlatform
|
||||
import com.vnidrop.app.ui.shell.AppShell
|
||||
import com.vnidrop.app.ui.theme.VniDropTheme
|
||||
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
@@ -290,6 +297,68 @@ class FoundationComposeTest {
|
||||
runOnIdle { assertEquals(AppDestination.Receive, selected) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nativeWindowBackdropShowsThroughDesktopChromeButNotMainContent() = runComposeUiTest {
|
||||
val sentinel = Color.Magenta
|
||||
var expectedMain = Color.Unspecified
|
||||
setContent {
|
||||
VniDropTheme(isDarkTheme = false) {
|
||||
expectedMain = LocalVniDropColors.current.backgroundDashCanvas
|
||||
Box(
|
||||
Modifier
|
||||
.size(width = 320.dp, height = 200.dp)
|
||||
.background(sentinel)
|
||||
.testTag("native-backdrop-shell"),
|
||||
) {
|
||||
AppShell(
|
||||
selectedDestination = AppDestination.Send,
|
||||
windowClass = WindowClass.Desktop,
|
||||
uiPlatform = UiPlatform.Windows,
|
||||
mainContentTopStartRadius = 20.dp,
|
||||
useNativeWindowBackdrop = true,
|
||||
onDestinationSelected = {},
|
||||
) {
|
||||
Text("Content")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val pixels = onNodeWithTag("native-backdrop-shell").captureToImage().toPixelMap()
|
||||
assertEquals(sentinel.toArgb(), pixels[pixels.width / 20, pixels.height * 9 / 10].toArgb())
|
||||
assertEquals(expectedMain.toArgb(), pixels[pixels.width * 19 / 20, pixels.height * 9 / 10].toArgb())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun desktopChromeKeepsSolidFallbackWithoutNativeBackdrop() = runComposeUiTest {
|
||||
var expectedSidebar = Color.Unspecified
|
||||
setContent {
|
||||
VniDropTheme(isDarkTheme = false) {
|
||||
expectedSidebar = LocalVniDropColors.current.backgroundSurface200
|
||||
Box(
|
||||
Modifier
|
||||
.size(width = 320.dp, height = 200.dp)
|
||||
.background(Color.Magenta)
|
||||
.testTag("solid-backdrop-shell"),
|
||||
) {
|
||||
AppShell(
|
||||
selectedDestination = AppDestination.Send,
|
||||
windowClass = WindowClass.Desktop,
|
||||
uiPlatform = UiPlatform.Windows,
|
||||
mainContentTopStartRadius = 20.dp,
|
||||
useNativeWindowBackdrop = false,
|
||||
onDestinationSelected = {},
|
||||
) {
|
||||
Text("Content")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val pixels = onNodeWithTag("solid-backdrop-shell").captureToImage().toPixelMap()
|
||||
assertEquals(expectedSidebar.toArgb(), pixels[pixels.width / 20, pixels.height * 9 / 10].toArgb())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun androidPagesUseStaticFeatureIconsWithoutTitleDescriptions() = runComposeUiTest {
|
||||
val actions = object : ReceiveInvitationActions {
|
||||
|
||||
Reference in New Issue
Block a user