Merge pull request #2780 from mstrofnone/fix/desktop-sidebar-scrollable
fix(desktop): make the single-pane navigation rail scrollable
This commit is contained in:
+56
-17
@@ -20,19 +20,24 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.desktop.ui.deck
|
package com.vitorpamplona.amethyst.desktop.ui.deck
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
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.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationRail
|
|
||||||
import androidx.compose.material3.NavigationRailItem
|
import androidx.compose.material3.NavigationRailItem
|
||||||
|
import androidx.compose.material3.NavigationRailItemDefaults
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.VerticalDivider
|
import androidx.compose.material3.VerticalDivider
|
||||||
@@ -105,13 +110,46 @@ fun SinglePaneLayout(
|
|||||||
|
|
||||||
Row(modifier = modifier.fillMaxSize()) {
|
Row(modifier = modifier.fillMaxSize()) {
|
||||||
if (!isImmersive) {
|
if (!isImmersive) {
|
||||||
NavigationRail(
|
// Custom navigation rail with a scrollable items area so all pinned
|
||||||
modifier = Modifier.width(80.dp).fillMaxHeight(),
|
// screens (Home, Reads, Notifications, ...) remain reachable when
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
// the window is short. Bottom status indicators stay anchored.
|
||||||
// macOS: push rail items below the traffic lights.
|
//
|
||||||
header = { Spacer(Modifier.height(titleBarInsetTop)) },
|
// We don't use Material3 `NavigationRail` directly here because its
|
||||||
) {
|
// internal `Column` is not scrollable — when there are more pinned
|
||||||
|
// items than fit vertically, items at the bottom of the list (and
|
||||||
|
// the "More" button) get clipped on small windows. We replicate the
|
||||||
|
// rail's container styling via NavigationRailItemDefaults so item
|
||||||
|
// visuals are unchanged.
|
||||||
|
val railScrollState = rememberScrollState()
|
||||||
val pinnedScreens by pinnedNavBarState.pinnedScreens.collectAsState()
|
val pinnedScreens by pinnedNavBarState.pinnedScreens.collectAsState()
|
||||||
|
val torState = LocalTorState.current
|
||||||
|
val allAccountsState by accountManager.allAccounts.collectAsState()
|
||||||
|
val singlePaneScope = rememberCoroutineScope()
|
||||||
|
var showAddAccountDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.width(80.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer),
|
||||||
|
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
// macOS: push rail items below the traffic lights.
|
||||||
|
Spacer(Modifier.height(titleBarInsetTop))
|
||||||
|
|
||||||
|
// Scrollable region for pinned screens + "More" launcher.
|
||||||
|
// Takes all remaining space above the fixed bottom controls
|
||||||
|
// (weight(1f)) and scrolls vertically when items overflow.
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(railScrollState),
|
||||||
|
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
) {
|
||||||
pinnedScreens.forEach { screenType ->
|
pinnedScreens.forEach { screenType ->
|
||||||
// Rename "Home" to "Feeds" in the nav rail
|
// Rename "Home" to "Feeds" in the nav rail
|
||||||
val label = if (screenType == DeckColumnType.HomeFeed) "Feeds" else screenType.title()
|
val label = if (screenType == DeckColumnType.HomeFeed) "Feeds" else screenType.title()
|
||||||
@@ -136,6 +174,7 @@ fun SinglePaneLayout(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
colors = NavigationRailItemDefaults.colors(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,10 +195,16 @@ fun SinglePaneLayout(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
colors = NavigationRailItemDefaults.colors(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
// Fixed bottom controls — always visible regardless of how
|
||||||
|
// many items are in the scrollable region above.
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
// Relay health — shows elapsed time since last event (hidden when <30s)
|
// Relay health — shows elapsed time since last event (hidden when <30s)
|
||||||
RelayHealthIndicator(
|
RelayHealthIndicator(
|
||||||
lastEventReceivedAt = lastRelayEventAt,
|
lastEventReceivedAt = lastRelayEventAt,
|
||||||
@@ -172,8 +217,6 @@ fun SinglePaneLayout(
|
|||||||
modifier = Modifier.padding(bottom = 4.dp),
|
modifier = Modifier.padding(bottom = 4.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tor status
|
|
||||||
val torState = LocalTorState.current
|
|
||||||
TorStatusIndicator(
|
TorStatusIndicator(
|
||||||
status = torState.status,
|
status = torState.status,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -183,11 +226,6 @@ fun SinglePaneLayout(
|
|||||||
modifier = Modifier.padding(bottom = 4.dp),
|
modifier = Modifier.padding(bottom = 4.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Account switcher — at very bottom of rail
|
|
||||||
val allAccountsState by accountManager.allAccounts.collectAsState()
|
|
||||||
val singlePaneScope = rememberCoroutineScope()
|
|
||||||
var showAddAccountDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
AccountSwitcherDropdown(
|
AccountSwitcherDropdown(
|
||||||
activeNpub = accountManager.currentAccount()?.npub,
|
activeNpub = accountManager.currentAccount()?.npub,
|
||||||
allAccounts = allAccountsState,
|
allAccounts = allAccountsState,
|
||||||
@@ -205,6 +243,8 @@ fun SinglePaneLayout(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showAddAccountDialog) {
|
if (showAddAccountDialog) {
|
||||||
AddAccountDialog(
|
AddAccountDialog(
|
||||||
@@ -219,7 +259,6 @@ fun SinglePaneLayout(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!isImmersive) {
|
if (!isImmersive) {
|
||||||
VerticalDivider()
|
VerticalDivider()
|
||||||
|
|||||||
Reference in New Issue
Block a user