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.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@@ -54,6 +55,8 @@ fun App(
|
|||||||
dependencies: AppDependencies,
|
dependencies: AppDependencies,
|
||||||
windowChromeTopInset: Dp = 0.dp,
|
windowChromeTopInset: Dp = 0.dp,
|
||||||
windowContentTopStartRadius: Dp = 0.dp,
|
windowContentTopStartRadius: Dp = 0.dp,
|
||||||
|
useNativeWindowBackdrop: Boolean = false,
|
||||||
|
onResolvedDarkThemeChanged: (Boolean) -> Unit = {},
|
||||||
windowChrome: (@Composable () -> Unit)? = null,
|
windowChrome: (@Composable () -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val graphHolder = viewModel { AppGraphViewModel(dependencies) }
|
val graphHolder = viewModel { AppGraphViewModel(dependencies) }
|
||||||
@@ -140,13 +143,19 @@ fun App(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val darkTheme = rememberResolvedDarkTheme(appState.themeMode)
|
val darkTheme = rememberResolvedDarkTheme(appState.themeMode)
|
||||||
|
LaunchedEffect(darkTheme, onResolvedDarkThemeChanged) {
|
||||||
|
onResolvedDarkThemeChanged(darkTheme)
|
||||||
|
}
|
||||||
PlatformSystemAppearance(darkTheme)
|
PlatformSystemAppearance(darkTheme)
|
||||||
CompositionLocalProvider(LocalUiPlatform provides dependencies.environment.uiPlatform) {
|
CompositionLocalProvider(LocalUiPlatform provides dependencies.environment.uiPlatform) {
|
||||||
VniDropTheme(isDarkTheme = darkTheme) {
|
VniDropTheme(isDarkTheme = darkTheme) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(LocalVniDropColors.current.backgroundSurface200),
|
.background(
|
||||||
|
if (useNativeWindowBackdrop) Color.Transparent
|
||||||
|
else LocalVniDropColors.current.backgroundSurface200,
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -171,6 +180,7 @@ fun App(
|
|||||||
windowClass = windowClass,
|
windowClass = windowClass,
|
||||||
uiPlatform = dependencies.environment.uiPlatform,
|
uiPlatform = dependencies.environment.uiPlatform,
|
||||||
mainContentTopStartRadius = windowContentTopStartRadius,
|
mainContentTopStartRadius = windowContentTopStartRadius,
|
||||||
|
useNativeWindowBackdrop = useNativeWindowBackdrop,
|
||||||
onDestinationSelected = appViewModel::selectDestination,
|
onDestinationSelected = appViewModel::selectDestination,
|
||||||
overlay = {
|
overlay = {
|
||||||
VniDropSnackbarHost(graph.messages, Modifier.align(Alignment.BottomCenter))
|
VniDropSnackbarHost(graph.messages, Modifier.align(Alignment.BottomCenter))
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ fun AppSidebarNavigation(
|
|||||||
style: NavigationStyle,
|
style: NavigationStyle,
|
||||||
onDestinationSelected: (AppDestination) -> Unit,
|
onDestinationSelected: (AppDestination) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
useNativeWindowBackdrop: Boolean = false,
|
||||||
) {
|
) {
|
||||||
when (style) {
|
when (style) {
|
||||||
NavigationStyle.AndroidRail -> AndroidNavigationRail(selected, onDestinationSelected, modifier)
|
NavigationStyle.AndroidRail -> AndroidNavigationRail(selected, onDestinationSelected, modifier)
|
||||||
@@ -62,6 +63,7 @@ fun AppSidebarNavigation(
|
|||||||
selected = selected,
|
selected = selected,
|
||||||
onDestinationSelected = onDestinationSelected,
|
onDestinationSelected = onDestinationSelected,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
useNativeWindowBackdrop = useNativeWindowBackdrop,
|
||||||
)
|
)
|
||||||
NavigationStyle.AndroidBottomBar -> error("Bottom navigation is rendered by the phone shell")
|
NavigationStyle.AndroidBottomBar -> error("Bottom navigation is rendered by the phone shell")
|
||||||
}
|
}
|
||||||
@@ -103,13 +105,14 @@ private fun DesktopSidebarNavigation(
|
|||||||
selected: AppDestination,
|
selected: AppDestination,
|
||||||
onDestinationSelected: (AppDestination) -> Unit,
|
onDestinationSelected: (AppDestination) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
useNativeWindowBackdrop: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val colors = LocalVniDropColors.current
|
val colors = LocalVniDropColors.current
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(DesktopNavigationWidthDp.dp)
|
.width(DesktopNavigationWidthDp.dp)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.background(colors.backgroundSurface200)
|
.background(if (useNativeWindowBackdrop) Color.Transparent else colors.backgroundSurface200)
|
||||||
.padding(horizontal = 12.dp, vertical = 14.dp),
|
.padding(horizontal = 12.dp, vertical = 14.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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 androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vnidrop.app.UiPlatform
|
import com.vnidrop.app.UiPlatform
|
||||||
@@ -33,6 +34,7 @@ fun AppShell(
|
|||||||
windowClass: WindowClass,
|
windowClass: WindowClass,
|
||||||
uiPlatform: UiPlatform,
|
uiPlatform: UiPlatform,
|
||||||
mainContentTopStartRadius: Dp = 0.dp,
|
mainContentTopStartRadius: Dp = 0.dp,
|
||||||
|
useNativeWindowBackdrop: Boolean = false,
|
||||||
onDestinationSelected: (AppDestination) -> Unit,
|
onDestinationSelected: (AppDestination) -> Unit,
|
||||||
overlay: @Composable BoxScope.() -> Unit = {},
|
overlay: @Composable BoxScope.() -> Unit = {},
|
||||||
floatingAction: (@Composable BoxScope.() -> Unit)? = null,
|
floatingAction: (@Composable BoxScope.() -> Unit)? = null,
|
||||||
@@ -40,11 +42,13 @@ fun AppShell(
|
|||||||
) {
|
) {
|
||||||
val colors = LocalVniDropColors.current
|
val colors = LocalVniDropColors.current
|
||||||
val navigationStyle = navigationStyleFor(uiPlatform, windowClass)
|
val navigationStyle = navigationStyleFor(uiPlatform, windowClass)
|
||||||
|
val windowSurface = if (useNativeWindowBackdrop) Color.Transparent else colors.backgroundDashCanvas
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(colors.backgroundDashCanvas),
|
.background(windowSurface),
|
||||||
color = colors.backgroundDashCanvas,
|
color = windowSurface,
|
||||||
|
contentColor = colors.foregroundDefault,
|
||||||
) {
|
) {
|
||||||
if (navigationStyle == NavigationStyle.AndroidBottomBar) {
|
if (navigationStyle == NavigationStyle.AndroidBottomBar) {
|
||||||
PhoneShell(
|
PhoneShell(
|
||||||
@@ -59,6 +63,7 @@ fun AppShell(
|
|||||||
selectedDestination = selectedDestination,
|
selectedDestination = selectedDestination,
|
||||||
navigationStyle = navigationStyle,
|
navigationStyle = navigationStyle,
|
||||||
mainContentTopStartRadius = mainContentTopStartRadius,
|
mainContentTopStartRadius = mainContentTopStartRadius,
|
||||||
|
useNativeWindowBackdrop = useNativeWindowBackdrop,
|
||||||
onDestinationSelected = onDestinationSelected,
|
onDestinationSelected = onDestinationSelected,
|
||||||
overlay = overlay,
|
overlay = overlay,
|
||||||
floatingAction = floatingAction,
|
floatingAction = floatingAction,
|
||||||
@@ -73,6 +78,7 @@ private fun WideShell(
|
|||||||
selectedDestination: AppDestination,
|
selectedDestination: AppDestination,
|
||||||
navigationStyle: NavigationStyle,
|
navigationStyle: NavigationStyle,
|
||||||
mainContentTopStartRadius: Dp,
|
mainContentTopStartRadius: Dp,
|
||||||
|
useNativeWindowBackdrop: Boolean,
|
||||||
onDestinationSelected: (AppDestination) -> Unit,
|
onDestinationSelected: (AppDestination) -> Unit,
|
||||||
overlay: @Composable BoxScope.() -> Unit,
|
overlay: @Composable BoxScope.() -> Unit,
|
||||||
floatingAction: (@Composable BoxScope.() -> Unit)?,
|
floatingAction: (@Composable BoxScope.() -> Unit)?,
|
||||||
@@ -83,11 +89,18 @@ private fun WideShell(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.then(if (roundedContent) Modifier.background(colors.backgroundSurface200) else Modifier),
|
.then(
|
||||||
|
if (roundedContent && !useNativeWindowBackdrop) {
|
||||||
|
Modifier.background(colors.backgroundSurface200)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
AppSidebarNavigation(
|
AppSidebarNavigation(
|
||||||
selected = selectedDestination,
|
selected = selectedDestination,
|
||||||
style = navigationStyle,
|
style = navigationStyle,
|
||||||
|
useNativeWindowBackdrop = useNativeWindowBackdrop,
|
||||||
onDestinationSelected = onDestinationSelected,
|
onDestinationSelected = onDestinationSelected,
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
@@ -96,13 +109,12 @@ private fun WideShell(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.then(
|
.then(
|
||||||
if (roundedContent) {
|
if (roundedContent) {
|
||||||
Modifier
|
Modifier.clip(RoundedCornerShape(topStart = mainContentTopStartRadius))
|
||||||
.clip(RoundedCornerShape(topStart = mainContentTopStartRadius))
|
|
||||||
.background(colors.backgroundDashCanvas)
|
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
|
.background(colors.backgroundDashCanvas),
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
floatingAction?.invoke(this)
|
floatingAction?.invoke(this)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.onNodeWithText
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.performClick
|
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.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.unit.dp
|
||||||
import androidx.compose.ui.test.v2.runComposeUiTest
|
import androidx.compose.ui.test.v2.runComposeUiTest
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
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.platform.LocalUiPlatform
|
||||||
import com.vnidrop.app.ui.shell.AppShell
|
import com.vnidrop.app.ui.shell.AppShell
|
||||||
import com.vnidrop.app.ui.theme.VniDropTheme
|
import com.vnidrop.app.ui.theme.VniDropTheme
|
||||||
|
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
@@ -290,6 +297,68 @@ class FoundationComposeTest {
|
|||||||
runOnIdle { assertEquals(AppDestination.Receive, selected) }
|
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
|
@Test
|
||||||
fun androidPagesUseStaticFeatureIconsWithoutTitleDescriptions() = runComposeUiTest {
|
fun androidPagesUseStaticFeatureIconsWithoutTitleDescriptions() = runComposeUiTest {
|
||||||
val actions = object : ReceiveInvitationActions {
|
val actions = object : ReceiveInvitationActions {
|
||||||
|
|||||||
Reference in New Issue
Block a user