diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt b/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt index 9cc04049b..d894815aa 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt @@ -233,6 +233,19 @@ class MenuHost { @Composable fun rememberMenuHost(): MenuHost = remember { MenuHost() } +/** Under the anchor with left edges aligned, kept on screen: a dropdown that belongs to a tab. */ +private class BelowStartProvider(private val gap: Int) : PopupPositionProvider { + override fun calculatePosition(anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize): IntOffset { + var x = anchorBounds.left + if (x + popupContentSize.width > windowSize.width - 8) x = windowSize.width - 8 - popupContentSize.width + if (x < 8) x = 8 + var y = anchorBounds.bottom + gap + if (y + popupContentSize.height > windowSize.height - 8) y = anchorBounds.top - gap - popupContentSize.height + if (y < 8) y = 8 + return IntOffset(x, y) + } +} + private class BelowEndProvider(private val gap: Int) : PopupPositionProvider { override fun calculatePosition(anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize): IntOffset { var x = anchorBounds.right - popupContentSize.width @@ -245,7 +258,12 @@ private class BelowEndProvider(private val gap: Int) : PopupPositionProvider { } @Composable -fun AnchoredMenu(open: Boolean, onDismiss: () -> Unit, title: String? = null, note: String? = null, content: @Composable ColumnScope.(FocusRequester) -> Unit) { +fun AnchoredMenu( + open: Boolean, onDismiss: () -> Unit, title: String? = null, note: String? = null, + /** A tab's dropdown: under its anchor, left edges aligned, as wide as its entries (within [minWidth]..260 dp). */ + compact: Boolean = false, minWidth: androidx.compose.ui.unit.Dp = 120.dp, + content: @Composable ColumnScope.(FocusRequester) -> Unit, +) { val state = remember { MutableTransitionState(false) } state.targetState = open if (!state.currentState && !state.targetState && state.isIdle) return @@ -259,14 +277,14 @@ fun AnchoredMenu(open: Boolean, onDismiss: () -> Unit, title: String? = null, no } } val gap = with(LocalDensity.current) { 6.dp.roundToPx() } - val provider = remember(gap) { BelowEndProvider(gap) } + val provider = remember(gap, compact) { if (compact) BelowStartProvider(gap) else BelowEndProvider(gap) } val colors = MaterialTheme.colorScheme val pal = LocalPalette.current Popup(popupPositionProvider = provider, onDismissRequest = onDismiss, properties = PopupProperties(focusable = true)) { AnimatedVisibility( visibleState = state, - enter = fadeIn(Motion.tw(180)) + scaleIn(Motion.sp(0.7f), initialScale = 0.94f, transformOrigin = TransformOrigin(1f, 0f)), - exit = fadeOut(Motion.tw(140)) + scaleOut(Motion.tw(140), targetScale = 0.96f, transformOrigin = TransformOrigin(1f, 0f)), + enter = fadeIn(Motion.tw(180)) + scaleIn(Motion.sp(0.7f), initialScale = 0.94f, transformOrigin = TransformOrigin(if (compact) 0f else 1f, 0f)), + exit = fadeOut(Motion.tw(140)) + scaleOut(Motion.tw(140), targetScale = 0.96f, transformOrigin = TransformOrigin(if (compact) 0f else 1f, 0f)), label = "menu", ) { // A long list (a driver menu with its downloads) must not run off the screen: the menu is @@ -274,7 +292,7 @@ fun AnchoredMenu(open: Boolean, onDismiss: () -> Unit, title: String? = null, no val maxHeight = (androidx.compose.ui.platform.LocalConfiguration.current.screenHeightDp * 0.86f).dp Column( modifier = Modifier - .widthIn(min = 220.dp, max = 340.dp) + .then(if (compact) Modifier.widthIn(min = minWidth, max = 260.dp).width(androidx.compose.foundation.layout.IntrinsicSize.Max) else Modifier.widthIn(min = 220.dp, max = 340.dp)) .heightIn(max = maxHeight) .shadow(24.dp, RowShape, ambientColor = Color.Black, spotColor = Color.Black) .clip(RowShape) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt index 220342f6c..78af32e3e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt @@ -1,5 +1,6 @@ package com.droiddeck.launcher.ui +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Brush @@ -333,8 +334,8 @@ private fun Storefront( listOf("store" to stringResource(R.string.stores_tab_store), "library" to libraryLabel).filter { it.first in tabsFor(store) }, tab, onPick = { key -> if (key == "library") { if (tab == "library") filterOpen = !filterOpen else onTab(key) } else onTab(key) }, - menuFor = { key -> - if (key == "library") AnchoredMenu(filterOpen, onDismiss = { filterOpen = false }) { first -> + menuFor = { key, tabWidth -> + if (key == "library") AnchoredMenu(filterOpen, onDismiss = { filterOpen = false }, compact = true, minWidth = tabWidth) { first -> MenuItem(stringResource(R.string.stores_tab_count, stringResource(R.string.stores_tab_all), library.size), checked = !installedOnly, focusRequester = first) { installedOnly = false; filterOpen = false; onTab("library") } MenuItem(stringResource(R.string.stores_tab_count, stringResource(R.string.stores_tab_installed), installedItems.size), checked = installedOnly) { installedOnly = true; filterOpen = false; onTab("library") } } @@ -386,7 +387,7 @@ private fun Storefront( @Composable private fun SubTabs( tabs: List>, selected: String, onPick: (String) -> Unit, - menuFor: @Composable (String) -> Unit = {}, trailing: @Composable () -> Unit, + menuFor: @Composable (String, androidx.compose.ui.unit.Dp) -> Unit = { _, _ -> }, trailing: @Composable () -> Unit, ) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current @@ -398,7 +399,9 @@ private fun SubTabs( val src = remember { MutableInteractionSource() } val hot = rememberHot(src) val pick = { onPick(key) } - Box { + val density = androidx.compose.ui.platform.LocalDensity.current + var width by remember { mutableStateOf(0.dp) } + Box(Modifier.onSizeChanged { width = with(density) { it.width.toDp() } }) { Column( modifier = Modifier.paneItem("storetab:$key").clip(RoundedCornerShape(8.dp)) .background(if (hot) pal.signal.copy(alpha = 0.12f) else Color.Transparent) @@ -411,7 +414,7 @@ private fun SubTabs( Box(Modifier.padding(top = 4.dp).fillMaxWidth().height(2.dp).background(if (on) pal.signal else Color.Transparent)) } // A tab's own dropdown (Library's filter), anchored under it. - menuFor(key) + menuFor(key, width) } } } diff --git a/docs/progress-log.md b/docs/progress-log.md index 44785c486..49a7a4782 100644 --- a/docs/progress-log.md +++ b/docs/progress-log.md @@ -157,7 +157,8 @@ without it and Setup says "Stores engine not built"). GOG Library 33 / All 120): Library's dropdown filters All (every owned game, the default, the tab's count) or Installed, and the tab reads the filter ("Library (33)", "Installed (1)"); the old All view is gone. Amazon has Library alone. Search follows the filter; the shoulders cycle Store - and Library. + and Library. The dropdown opens under the Library tab, left edges aligned, as wide as its + entries (at least the tab, at most 260 dp) - `AnchoredMenu(compact = true)`. - **The Stores chip row paints nothing** (maintainer: a black box behind the chips): it sits over the page's own background; the content scrolling under it is clipped at its edge and faded there by the content's alpha once scrolled, never by a colour.