From be08e503140f8668ce021f51c6c450733bae2f35 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:50:51 +0000 Subject: [PATCH] ui: keep a gap before the first library tab Put an 8dp spacer at the start of the scrollable content and let the reveal leave that gap when scrolling back towards the first tab, so the selected pill no longer sits flush against the screen edge. The spacer scrolls away, so a tab that is half off is still cut at the edge. --- .../com/subtracks/ui/library/LibraryScreen.kt | 13 ++++--- .../com/subtracks/ui/LibraryScreenTest.kt | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt index 9637b9c9..cc1b3899 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt @@ -830,6 +830,7 @@ private fun ListOptionsSheet( private val BOTTOM_CLEARANCE = 80.dp private val TAB_BAR_CONTENT_HEIGHT = 52.dp +private val TAB_STRIP_LEADING_GAP = 8.dp private val TAB_ICON_SIZE = 24.dp private val TAB_VERTICAL_PADDING = 6.dp private val TOUCH_TARGET = 48.dp @@ -903,14 +904,15 @@ internal fun LibraryTabs( var viewportWidth by remember { mutableIntStateOf(0) } var contentWidth by remember { mutableIntStateOf(0) } var revealed by remember { mutableStateOf(false) } + val leadingGap = with(density) { TAB_STRIP_LEADING_GAP.toPx() } LaunchedEffect(pagerState.settledPage, viewportWidth, contentWidth, layoutDirection, bounds[pagerState.settledPage]) { if (viewportWidth == 0) return@LaunchedEffect val settled = bounds[pagerState.settledPage] ?: return@LaunchedEffect - val start = if (layoutDirection == LayoutDirection.Rtl) contentWidth - settled.right else settled.left - val end = if (layoutDirection == LayoutDirection.Rtl) contentWidth - settled.left else settled.right + val start = if (layoutDirection == LayoutDirection.Rtl) contentWidth - settled.right else leadingGap + settled.left + val end = if (layoutDirection == LayoutDirection.Rtl) contentWidth - settled.left else leadingGap + settled.right val target = when { - start < scrollState.value -> start + start - leadingGap < scrollState.value -> start - leadingGap end > scrollState.value + viewportWidth -> end - viewportWidth else -> null } @@ -939,13 +941,14 @@ internal fun LibraryTabs( Modifier .weight(1f) .onSizeChanged { viewportWidth = it.width } - .horizontalScroll(scrollState), + .horizontalScroll(scrollState) + .onSizeChanged { contentWidth = it.width }, verticalAlignment = Alignment.CenterVertically, ) { + Spacer(Modifier.width(TAB_STRIP_LEADING_GAP)) Box( modifier = Modifier - .onSizeChanged { contentWidth = it.width } .drawBehind { if (current != null && indicatorLeft != null && diff --git a/app/src/test/kotlin/com/subtracks/ui/LibraryScreenTest.kt b/app/src/test/kotlin/com/subtracks/ui/LibraryScreenTest.kt index 9819fc37..646d748f 100644 --- a/app/src/test/kotlin/com/subtracks/ui/LibraryScreenTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/LibraryScreenTest.kt @@ -464,6 +464,41 @@ class LibraryScreenTest { assertTabFullyVisible(LibraryTab.Playlists, labelWidth) } + @Test + @Config(qualifiers = "de-rDE-w320dp-h640dp-xhdpi") + fun returningToTheFirstTabLeavesAGapAtTheEdge() { + val selectedTab = mutableStateOf(LibraryTab.Playlists) + composeRule.setContent { + SubtracksTheme { + LibraryScreen( + selectedTab = selectedTab.value, + onTabSelected = { selectedTab.value = it }, + albums = remember { flowOf(PagingData.empty()) }, + artists = remember { flowOf(PagingData.empty()) }, + playlists = remember { flowOf(PagingData.empty()) }, + coverArt = { _, _ -> null }, + onAlbumClick = {}, + onArtistClick = {}, + onPlaylistClick = {}, + onSync = {}, + onOpenSettings = {}, + ) + } + } + composeRule.waitForIdle() + + composeRule.runOnIdle { selectedTab.value = LibraryTab.Home } + val expectedGap = with(composeRule.density) { TAB_EDGE_GAP.toPx() } + composeRule.waitUntil(5_000) { tabBounds(LibraryTab.Home).left <= expectedGap + 1f } + + assertEquals( + "the first tab should keep a gap from the screen edge", + expectedGap, + tabBounds(LibraryTab.Home).left, + 1f, + ) + } + private fun assertTabFullyVisible( tab: LibraryTab, labelWidth: Float, @@ -549,3 +584,4 @@ class LibraryScreenTest { } private val TAB_LABEL_CHROME = 56.dp +private val TAB_EDGE_GAP = 8.dp