Fix medium sized devices using bar navigation instead of rail navigation.

This commit is contained in:
jeffrey-signal
2026-06-02 10:35:36 -04:00
committed by Alex Hart
parent c2f8261419
commit b02210c166
10 changed files with 66 additions and 54 deletions
@@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.window.core.layout.WindowSizeClass
import org.signal.core.ui.WindowBreakpoint
import org.signal.core.ui.getWindowBreakpoint
import org.signal.core.ui.rememberIsSplitPane
@@ -78,7 +79,7 @@ data class MainContentLayoutData(
val isSplitPane = resources.rememberIsSplitPane()
return remember(windowSizeClass, mode, breakpoint, isSplitPane) {
val isLargeWindowSize = breakpoint.isLargeWindow
val isLargeWindowSize = breakpoint is WindowBreakpoint.Large
MainContentLayoutData(
shape = when {
@@ -81,8 +81,9 @@ enum class NavigationType {
val windowBreakpoint = remember(config) { resources.getWindowBreakpoint() }
return when (windowBreakpoint) {
WindowBreakpoint.SMALL, WindowBreakpoint.MEDIUM -> BAR
WindowBreakpoint.LARGE_WIDTH, WindowBreakpoint.LARGE_HEIGHT -> RAIL
is WindowBreakpoint.Small -> BAR
is WindowBreakpoint.Medium -> if (windowBreakpoint.isWidthExpanded) RAIL else BAR
is WindowBreakpoint.Large -> RAIL
}
}
}
@@ -54,47 +54,61 @@ fun rememberWindowBreakpoint(): WindowBreakpoint {
* Determines the device's form factor based on the current [Resources] and window size class.
*
* This function uses several heuristics:
* - Returns [WindowBreakpoint.SMALL] if the width or height is compact
* - Otherwise, falls back to aspect ratio heuristics: wide windows use [WindowBreakpoint.LARGE_WIDTH], tall windows use [WindowBreakpoint.LARGE_HEIGHT], else [WindowBreakpoint.MEDIUM].
* - Returns [WindowBreakpoint.Small] if the window width or height is compact.
* - Otherwise, falls back to window aspect ratio heuristics:
* aspect ratio < [TABLET_ASPECT_RATIO] is [WindowBreakpoint.Medium]
* aspect ratio >= [TABLET_ASPECT_RATIO] is [WindowBreakpoint.Large]
*
* @return the inferred [WindowBreakpoint] for the current device.
*/
fun Resources.getWindowBreakpoint(): WindowBreakpoint {
val windowSizeClass = getWindowSizeClass()
val isWidthExpanded = windowSizeClass.isWidthExpanded
val isHeightExpanded = windowSizeClass.isHeightExpanded
if (windowSizeClass.isWidthCompact || windowSizeClass.isHeightCompact) {
return WindowBreakpoint.SMALL
return WindowBreakpoint.Small(isWidthExpanded, isHeightExpanded)
}
val numerator = maxOf(displayMetrics.widthPixels, displayMetrics.heightPixels)
val denominator = minOf(displayMetrics.widthPixels, displayMetrics.heightPixels)
val aspectRatio = numerator.toFloat() / denominator
return when {
aspectRatio < TABLET_ASPECT_RATIO -> WindowBreakpoint.MEDIUM
else -> {
if (displayMetrics.widthPixels >= displayMetrics.heightPixels) {
WindowBreakpoint.LARGE_WIDTH
} else {
WindowBreakpoint.LARGE_HEIGHT
}
}
return if (aspectRatio < TABLET_ASPECT_RATIO) {
WindowBreakpoint.Medium(isWidthExpanded, isHeightExpanded)
} else {
WindowBreakpoint.Large(isWidthExpanded, isHeightExpanded)
}
}
/**
* Indicates the general form factor of the device for responsive UI purposes.
*
* - [SMALL]: A window with a compact width or height, typical of phone-sized devices.
* - [MEDIUM]: A window where neither width nor height is compact or expanded, typical of foldables.
* - [LARGE_WIDTH]: A window with expanded width and medium height, typical of tablets in landscape orientation.
* - [LARGE_HEIGHT]: A window with medium width and expanded height, typical of tablets in portrait orientation.
* - [Small]: A window with a compact width or height, typical of phone-sized devices.
* - [Medium]: A non-compact window with a near-square aspect ratio (< [TABLET_ASPECT_RATIO]), typical of open foldable devices.
* - [Large]: A window with an aspect ratio >= [TABLET_ASPECT_RATIO], typical of tablets.
*/
enum class WindowBreakpoint(val isLargeWindow: Boolean) {
SMALL(isLargeWindow = false),
MEDIUM(isLargeWindow = false),
LARGE_WIDTH(isLargeWindow = true),
LARGE_HEIGHT(isLargeWindow = true)
sealed interface WindowBreakpoint {
/** True when the [WindowSizeClass] width is >= the expanded breakpoint (e.g., a tablet in landscape orientation) */
val isWidthExpanded: Boolean
/** True when the [WindowSizeClass] height is >= the expanded breakpoint (e.g., a tablet in landscape orientation) */
val isHeightExpanded: Boolean
data class Small(
override val isWidthExpanded: Boolean,
override val isHeightExpanded: Boolean
) : WindowBreakpoint
data class Medium(
override val isWidthExpanded: Boolean,
override val isHeightExpanded: Boolean
) : WindowBreakpoint
data class Large(
override val isWidthExpanded: Boolean,
override val isHeightExpanded: Boolean
) : WindowBreakpoint
}
@Composable
@@ -117,8 +131,9 @@ fun Resources.isSplitPane(
return true
}
return when (getWindowBreakpoint()) {
WindowBreakpoint.SMALL, WindowBreakpoint.LARGE_HEIGHT -> false
WindowBreakpoint.MEDIUM, WindowBreakpoint.LARGE_WIDTH -> true
return when (val breakpoint = getWindowBreakpoint()) {
is WindowBreakpoint.Small -> false
is WindowBreakpoint.Medium -> true
is WindowBreakpoint.Large -> breakpoint.isWidthExpanded
}
}
@@ -43,7 +43,7 @@ fun HSVColorBar(
onColorChanged: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val orientation = if (rememberWindowBreakpoint() == WindowBreakpoint.SMALL) {
val orientation = if (rememberWindowBreakpoint() is WindowBreakpoint.Small) {
ColorBarOrientation.HORIZONTAL
} else {
ColorBarOrientation.VERTICAL
@@ -292,7 +292,7 @@ private fun OrientedImageEditorToolbar(
content: @Composable () -> Unit
) {
val windowBreakpoint = rememberWindowBreakpoint()
val isRow = windowBreakpoint == WindowBreakpoint.SMALL
val isRow = windowBreakpoint is WindowBreakpoint.Small
if (isRow) {
Row(modifier = modifier.height(48.dp)) {
@@ -59,7 +59,7 @@ fun MediaEditScreen(
.fillMaxSize()
.navigationBarsPadding()
) {
val isSmallWindowBreakpoint = rememberWindowBreakpoint() == WindowBreakpoint.SMALL
val isSmallWindowBreakpoint = rememberWindowBreakpoint() is WindowBreakpoint.Small
val controllers = remember { EditorController.Container() }
val currentController = state.focusedMedia?.let {
@@ -125,11 +125,10 @@ object RegistrationScaffold {
@Composable
fun rememberLayoutParams(): Params {
return when (rememberWindowBreakpoint()) {
WindowBreakpoint.SMALL -> smallLayoutParams
WindowBreakpoint.MEDIUM -> mediumLayoutParams
WindowBreakpoint.LARGE_WIDTH -> largeWidthLayoutParams
WindowBreakpoint.LARGE_HEIGHT -> largeHeightLayoutParams
return when (val breakpoint = rememberWindowBreakpoint()) {
is WindowBreakpoint.Small -> smallLayoutParams
is WindowBreakpoint.Medium -> mediumLayoutParams
is WindowBreakpoint.Large -> if (breakpoint.isWidthExpanded) largeWidthLayoutParams else largeHeightLayoutParams
}
}
@@ -417,10 +417,9 @@ fun getQrOuterBorderSize(isInOverlay: Boolean): Dp {
val breakpoint = rememberWindowBreakpoint()
return when (breakpoint) {
WindowBreakpoint.SMALL -> 296.dp
WindowBreakpoint.MEDIUM -> 296.dp
WindowBreakpoint.LARGE_WIDTH -> 364.dp
WindowBreakpoint.LARGE_HEIGHT -> 364.dp
is WindowBreakpoint.Small -> 296.dp
is WindowBreakpoint.Medium -> 296.dp
is WindowBreakpoint.Large -> 364.dp
}
}
@@ -432,10 +431,9 @@ fun getQrInnerBorderSize(isInOverlay: Boolean): Dp {
val breakpoint = rememberWindowBreakpoint()
return when (breakpoint) {
WindowBreakpoint.SMALL -> 232.dp
WindowBreakpoint.MEDIUM -> 232.dp
WindowBreakpoint.LARGE_WIDTH -> 284.dp
WindowBreakpoint.LARGE_HEIGHT -> 284.dp
is WindowBreakpoint.Small -> 232.dp
is WindowBreakpoint.Medium -> 232.dp
is WindowBreakpoint.Large -> 284.dp
}
}
@@ -447,10 +445,9 @@ fun getQrCodeSize(isInOverlay: Boolean): Dp {
val breakpoint = rememberWindowBreakpoint()
return when (breakpoint) {
WindowBreakpoint.SMALL -> 208.dp
WindowBreakpoint.MEDIUM -> 208.dp
WindowBreakpoint.LARGE_WIDTH -> 256.dp
WindowBreakpoint.LARGE_HEIGHT -> 256.dp
is WindowBreakpoint.Small -> 208.dp
is WindowBreakpoint.Medium -> 208.dp
is WindowBreakpoint.Large -> 256.dp
}
}
@@ -173,10 +173,9 @@ private fun FooterContent(
val breakpoint = rememberWindowBreakpoint()
when (breakpoint) {
WindowBreakpoint.SMALL -> StackedFooter(params, modifier, onEvent)
WindowBreakpoint.MEDIUM -> StackedFooter(params, modifier, onEvent)
WindowBreakpoint.LARGE_WIDTH -> InlineFooter(modifier, onEvent)
WindowBreakpoint.LARGE_HEIGHT -> InlineFooter(modifier, onEvent)
is WindowBreakpoint.Small -> StackedFooter(params, modifier, onEvent)
is WindowBreakpoint.Medium -> StackedFooter(params, modifier, onEvent)
is WindowBreakpoint.Large -> InlineFooter(modifier, onEvent)
}
}
@@ -84,7 +84,7 @@ fun WelcomeScreen(
val onTermsAndPrivacyClick = { onEvent(WelcomeScreenEvents.ViewTermsAndPrivacy) }
when (windowBreakpoint) {
WindowBreakpoint.SMALL -> {
is WindowBreakpoint.Small -> {
CompactLayout(
onEvent = onEvent,
onRestoreOrTransferClick = onRestoreOrTransferClick,
@@ -93,7 +93,7 @@ fun WelcomeScreen(
)
}
WindowBreakpoint.MEDIUM -> {
is WindowBreakpoint.Medium -> {
MediumLayout(
onEvent = onEvent,
onRestoreOrTransferClick = onRestoreOrTransferClick,
@@ -102,7 +102,7 @@ fun WelcomeScreen(
)
}
WindowBreakpoint.LARGE_WIDTH, WindowBreakpoint.LARGE_HEIGHT -> {
is WindowBreakpoint.Large -> {
LargeLayout(
isLinkAndSyncAvailable = isLinkAndSyncAvailable,
onEvent = onEvent,