Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,48 +1,143 @@
package com.plainstudio.stackcasino.ui.components

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AccountBalanceWallet
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Newspaper
import androidx.compose.material.icons.outlined.Person
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.plainstudio.stackcasino.R
import com.plainstudio.stackcasino.navigation.PrimaryTab
import com.plainstudio.stackcasino.ui.theme.AccentViolet
import com.plainstudio.stackcasino.ui.theme.SurfaceBase
import com.plainstudio.stackcasino.ui.theme.SurfaceOutline
import com.plainstudio.stackcasino.ui.theme.TextMedium

/**
* Material 3 bottom navigation bar bound to the five top-level
* destinations defined in [PrimaryTab]. Tab visibility is owned by the
* caller: render this only when the current destination is one of
* [PrimaryTab.route].
* Bottom navigation bar mirroring the mockup spec
* (mockup/js/components.js, `bottomNav`):
*
* nav: h-16, border-t border-line, bg-[#0B0B12], grid 5 cols
* button: stacked icon + 9px tracked label, violet when active,
* muted otherwise
* icon: 18dp, stroked (no fill), stroke-width 2
*
* Drawn as a custom [Row] instead of `androidx.compose.material3.NavigationBar`
* because the Material 3 default surface, indicator pill and icon
* tinting would all need overrides; a plain row matches the mockup
* exactly with less ceremony.
*
* Tab visibility is owned by the caller: render this only when the
* current destination is one of [PrimaryTab.route].
*/
@Composable
fun StackBottomBar(
currentRoute: String?,
onTabSelected: (PrimaryTab) -> Unit,
modifier: Modifier = Modifier,
) {
NavigationBar {
PrimaryTab.entries.forEach { tab ->
NavigationBarItem(
selected = currentRoute == tab.route.path,
onClick = { onTabSelected(tab) },
icon = { Icon(imageVector = tab.icon, contentDescription = tab.label) },
label = { Text(tab.label) },
)
Surface(
modifier =
modifier
.fillMaxWidth()
.height(BarHeight),
color = SurfaceBase,
) {
Row(
modifier =
Modifier
.fillMaxSize()
.drawBehind {
val strokePx = TopBorderWidth.toPx()
// drawLine centers the stroke on the given coordinate;
// offsetting by half its width keeps the entire border
// visible inside the row instead of being clipped at
// the top edge.
val centerY = strokePx / 2f
drawLine(
color = SurfaceOutline,
start = Offset(0f, centerY),
end = Offset(size.width, centerY),
strokeWidth = strokePx,
)
},
) {
PrimaryTab.entries.forEach { tab ->
BottomNavTab(
tab = tab,
isActive = currentRoute == tab.route.path,
onClick = { onTabSelected(tab) },
modifier = Modifier.weight(1f),
)
}
}
}
}

private val PrimaryTab.icon: ImageVector
@Composable
private fun BottomNavTab(
tab: PrimaryTab,
isActive: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val tint = if (isActive) AccentViolet else TextMedium
Column(
modifier =
modifier
.fillMaxHeight()
.clickable(onClick = onClick),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Icon(
painter = painterResource(tab.iconRes),
// The label below already announces the tab to TalkBack; a
// contentDescription on the icon would cause it to read twice.
contentDescription = null,
tint = tint,
modifier = Modifier.size(IconSize),
)
Spacer(modifier = Modifier.height(IconLabelGap))
Text(
text = tab.label.uppercase(),
color = tint,
fontSize = LabelFontSize,
letterSpacing = LabelLetterSpacing,
)
}
}

@get:DrawableRes
private val PrimaryTab.iconRes: Int
get() =
when (this) {
PrimaryTab.Lobby -> Icons.Outlined.Home
PrimaryTab.Wallet -> Icons.Outlined.AccountBalanceWallet
PrimaryTab.History -> Icons.Outlined.History
PrimaryTab.News -> Icons.Outlined.Newspaper
PrimaryTab.Profile -> Icons.Outlined.Person
PrimaryTab.Lobby -> R.drawable.ic_tab_lobby
PrimaryTab.Wallet -> R.drawable.ic_tab_wallet
PrimaryTab.History -> R.drawable.ic_tab_history
PrimaryTab.News -> R.drawable.ic_tab_news
PrimaryTab.Profile -> R.drawable.ic_tab_profile
}

private val BarHeight = 64.dp
private val TopBorderWidth = 1.dp
private val IconSize = 18.dp
private val IconLabelGap = 4.dp
private val LabelFontSize = 9.sp
private val LabelLetterSpacing = 1.2.sp
20 changes: 20 additions & 0 deletions app/src/main/res/drawable/ic_tab_history.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!--
Clock face. Ported from mockup/js/components.js bottomNav.icons.history:
<circle cx="12" cy="12" r="9"/>
<path d="M12 7v5l3 2"/>
The circle is expressed as two semicircle arcs to keep the path data
pure (vector drawables do not have a <circle> primitive).
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M3,12 a9,9 0 1,0 18,0 a9,9 0 1,0 -18,0 z M12,7 v5 l3,2" />
</vector>
18 changes: 18 additions & 0 deletions app/src/main/res/drawable/ic_tab_lobby.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!--
House outline. Ported verbatim from mockup/js/components.js
bottomNav.icons.lobby: "M3 11l9-7 9 7v10h-6v-6H9v6H3z".
Tint is applied by the Icon composable at render time.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M3,11 l9,-7 9,7 v10 h-6 v-6 H9 v6 H3 z" />
</vector>
19 changes: 19 additions & 0 deletions app/src/main/res/drawable/ic_tab_news.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!--
Document with text lines. Ported from mockup/js/components.js
bottomNav.icons.news:
<rect x="3" y="4" width="18" height="16"/>
<path d="M7 8h10M7 12h10M7 16h6"/>
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M3,4 h18 v16 h-18 z M7,8 h10 M7,12 h10 M7,16 h6" />
</vector>
20 changes: 20 additions & 0 deletions app/src/main/res/drawable/ic_tab_profile.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!--
Person silhouette. Ported from mockup/js/components.js
bottomNav.icons.profile:
<circle cx="12" cy="8" r="4"/>
<path d="M4 21c0-4 4-7 8-7s8 3 8 7"/>
The head circle is expressed as two semicircle arcs.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M8,8 a4,4 0 1,0 8,0 a4,4 0 1,0 -8,0 z M4,21 c0,-4 4,-7 8,-7 s8,3 8,7" />
</vector>
19 changes: 19 additions & 0 deletions app/src/main/res/drawable/ic_tab_wallet.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!--
Wallet outline with the small clasp on the right edge. Ported from
mockup/js/components.js bottomNav.icons.wallet:
<rect x="3" y="6" width="18" height="14"/>
<path d="M16 13h3"/>
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M3,6 h18 v14 h-18 z M16,13 h3" />
</vector>
Loading