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
9 changes: 8 additions & 1 deletion ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/FT8AFApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,14 @@ fun FT8AFApp(mainViewModel: MainViewModel) {
showCatChip = showCatChip,
catState = catState,
onReconnectCat = onReconnectCatAction,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp),
// Bottom padding separates the status line from the sheet's rounded top edge
// below it — butted together, the rounding read as if the line were clipped.
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
top = 8.dp,
bottom = 10.dp,
),
)
Spacer(
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package radio.ks3ckc.ft8af.ui.components

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
Expand Down Expand Up @@ -73,11 +69,37 @@ internal fun drawerTargetExpanded(
/** The vertical drag distance on the handle that commits to toggling the drawer. */
internal val DrawerDragThreshold = 48.dp

/** Height of the grab handle strip (grabber bar + the collapsed "More controls" hint). */
internal val DrawerHandleHeight = 36.dp

/** Gap between the handle strip and the first control row. */
internal val DrawerContentTopGap = 10.dp

/**
* Padding below the last visible control row. In the collapsed peek this is the breathing room
* between the Call CQ / Hunt row and the tab bar the sheet sits on, so it must be comfortably
* bigger than nothing and comfortably smaller than [DrawerRowSpacing] (see below).
*/
internal val DrawerContentBottomPad = 14.dp

/**
* The collapsed height of the drawer — the handle (with its "More controls" hint) plus the primary
* Call CQ + Hunt row, stopping before the next row so nothing peeks through when collapsed.
* Vertical gap between control rows. Deliberately larger than [DrawerContentBottomPad]: the
* collapsed sheet is clipped [DrawerContentBottomPad] past the primary row's bottom edge, so the
* next row only stays fully hidden while the gap exceeds that padding.
*/
internal val OperateDrawerPeekHeight = 120.dp
internal val DrawerRowSpacing = 18.dp

/**
* The collapsed height of the drawer — the handle (with its "More controls" hint), the primary
* Call CQ + Hunt row, and equal-feeling padding above and below it, stopping short of the next row
* so nothing peeks through when collapsed.
*
* Derived from the parts rather than hard-coded (it was a flat 120.dp, which left the 72.dp button
* row butted against the handle with 6.dp under it — the row read as jammed onto the tab bar) so
* the peek can never clip the primary row or reveal the row below it.
*/
internal val OperateDrawerPeekHeight =
DrawerHandleHeight + DrawerContentTopGap + OperatePrimaryRowHeight + DrawerContentBottomPad

/**
* The operate-controls drawer (design 3a/3b) as a *single* bottom sheet that grows in place.
Expand Down Expand Up @@ -199,8 +221,13 @@ fun OperateControlsDrawer(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
.padding(
start = 16.dp,
end = 16.dp,
top = DrawerContentTopGap,
bottom = DrawerContentBottomPad,
),
verticalArrangement = Arrangement.spacedBy(DrawerRowSpacing),
) {
// ---- Primary row (visible in the peek): Call CQ / Stop + Hunt ----
CqHuntRow(
Expand Down Expand Up @@ -246,9 +273,13 @@ fun OperateControlsDrawer(

/**
* The drawer's grab handle: a centered grabber bar plus — while collapsed — an up-chevron and
* "More controls" caption that gently bob to advertise that the sheet pulls up. A tap toggles the
* drawer; a vertical drag is fed live to [onDrag] (dy, negative = up) and settled in [onDragEnd]
* with the total drag distance.
* "More controls" caption advertising that the sheet pulls up. A tap toggles the drawer; a
* vertical drag is fed live to [onDrag] (dy, negative = up) and settled in [onDragEnd] with the
* total drag distance.
*
* The hint used to bob up and down on an infinite animation. It sat directly above the primary
* action row, where a caption drifting against fixed buttons read as uneven spacing rather than as
* an affordance, so it is static now — the chevron carries the message.
*/
@Composable
private fun DrawerHandle(
Expand All @@ -263,19 +294,10 @@ private fun DrawerHandle(
val collapseLabel = stringResource(R.string.controls_drawer_collapse)
val label = if (expanded) collapseLabel else expandLabel

// Gentle up-and-down bob on the hint, only while collapsed, to draw the eye to the affordance.
val bobTransition = rememberInfiniteTransition(label = "drawer-hint-bob")
val bob by bobTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(tween(900), RepeatMode.Reverse),
label = "drawer-hint-bob-offset",
)

Column(
modifier = modifier
.fillMaxWidth()
.height(42.dp)
.height(DrawerHandleHeight)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
Expand All @@ -293,7 +315,7 @@ private fun DrawerHandle(
)
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(3.dp, Alignment.CenterVertically),
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
) {
Box(
modifier = Modifier
Expand All @@ -307,7 +329,6 @@ private fun DrawerHandle(
FT8AFIcons.ChevronDown(size = 14.dp, color = TextFaint, strokeWidth = 2.2f)
} else {
Row(
modifier = Modifier.offset(y = (bob * -3f).dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ internal data class TxStripActionState(
val cqIsStop: Boolean,
)

/**
* Height of the primary action row — the Call CQ button and the Hunt tile beside it. Shared so
* the collapsing drawer can derive its peek height from the real button height instead of
* repeating the number (see OperateDrawerPeekHeight).
*/
internal val OperatePrimaryRowHeight = 72.dp

/**
* Clamp a volume value after a +/- step to the 0–100 range.
* Extracted so it can be unit-tested without Compose.
Expand Down Expand Up @@ -528,7 +535,7 @@ internal fun CallCqButton(

Row(
modifier = modifier
.height(72.dp)
.height(OperatePrimaryRowHeight)
.clip(RoundedCornerShape(14.dp))
.background(background)
.combinedClickable(
Expand Down Expand Up @@ -629,7 +636,7 @@ internal fun HuntTile(
val contentAlpha = if (huntDisabled) 0.4f else 1f
Column(
modifier = modifier
.height(72.dp)
.height(OperatePrimaryRowHeight)
.clip(RoundedCornerShape(14.dp))
.background(if (huntDisabled) BgSurface3.copy(alpha = 0.4f) else BgSurface3)
.border(1.dp, Border, RoundedCornerShape(14.dp))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package radio.ks3ckc.ft8af.ui.components

import androidx.compose.ui.unit.dp
import com.google.common.truth.Truth.assertThat
import org.junit.Test

/**
* Unit tests for [drawerTargetExpanded] — the pure gesture rule deciding whether the
* operate-controls drawer (design 3a/3b) settles open or collapsed after a drag on its handle.
* No Android/Compose runtime needed. Sign convention: negative dy = upward drag.
* operate-controls drawer (design 3a/3b) settles open or collapsed after a drag on its handle —
* plus the collapsed-peek geometry invariants. No Android/Compose runtime needed. Sign convention:
* negative dy = upward drag.
*/
class OperateControlsDrawerTest {

Expand Down Expand Up @@ -67,4 +69,36 @@ class OperateControlsDrawerTest {
assertThat(drawerTargetExpanded(currentlyExpanded = true, dragDy = 0f, thresholdPx = threshold))
.isTrue()
}

// ---- Collapsed-peek geometry (see OperateDrawerPeekHeight) ----

@Test
fun `peek height is the handle, the primary row and its padding`() {
assertThat(OperateDrawerPeekHeight).isEqualTo(
DrawerHandleHeight + DrawerContentTopGap + OperatePrimaryRowHeight +
DrawerContentBottomPad
)
}

@Test
fun `peek height clears the whole primary row`() {
// Anything less and the collapsed sheet clips the Call CQ / Hunt buttons.
assertThat(OperateDrawerPeekHeight)
.isGreaterThan(DrawerHandleHeight + DrawerContentTopGap + OperatePrimaryRowHeight)
}

@Test
fun `row gap exceeds the bottom padding so the next row cannot peek through`() {
// The sheet is clipped DrawerContentBottomPad past the primary row's bottom edge; the
// following row starts DrawerRowSpacing past it, so the gap has to be the larger of the two.
assertThat(DrawerRowSpacing).isGreaterThan(DrawerContentBottomPad)
}

@Test
fun `primary row has breathing room above and below it`() {
// The 120.dp peek this replaced left 0.dp above the buttons and 6.dp below, which is what
// made the collapsed drawer look jammed onto the tab bar.
assertThat(DrawerContentTopGap).isAtLeast(8.dp)
assertThat(DrawerContentBottomPad).isAtLeast(8.dp)
}
}
Loading