diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt index ca26b4d441..1059bad4e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt @@ -12,9 +12,12 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape +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.isSplitPane private val MEDIUM_CONTENT_CORNERS = 18.dp @@ -70,25 +73,27 @@ data class MainContentLayoutData( @Composable fun rememberContentLayoutData(mode: MainToolbarMode): MainContentLayoutData { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val resources = LocalResources.current + val breakpoint = resources.getWindowBreakpoint() - return remember(windowSizeClass, mode) { + return remember(windowSizeClass, mode, breakpoint) { val isSplitPane = windowSizeClass.isSplitPane() - val isWidthExpanded = windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND) + val isLargeWindowSize = breakpoint == WindowBreakpoint.LARGE MainContentLayoutData( shape = when { !isSplitPane -> RectangleShape - isWidthExpanded -> RoundedCornerShape(EXTENDED_CONTENT_CORNERS) + isLargeWindowSize -> RoundedCornerShape(EXTENDED_CONTENT_CORNERS) else -> RoundedCornerShape(MEDIUM_CONTENT_CORNERS) }, navigationBarShape = when { !isSplitPane -> RectangleShape - isWidthExpanded -> RoundedCornerShape(0.dp, 0.dp, EXTENDED_CONTENT_CORNERS, EXTENDED_CONTENT_CORNERS) + isLargeWindowSize -> RoundedCornerShape(0.dp, 0.dp, EXTENDED_CONTENT_CORNERS, EXTENDED_CONTENT_CORNERS) else -> RoundedCornerShape(0.dp, 0.dp, MEDIUM_CONTENT_CORNERS, MEDIUM_CONTENT_CORNERS) }, partitionWidth = when { !isSplitPane -> 0.dp - isWidthExpanded -> 24.dp + isLargeWindowSize -> 24.dp else -> 13.dp }, listPaddingStart = when { @@ -102,7 +107,7 @@ data class MainContentLayoutData( }, detailPaddingEnd = when { !isSplitPane -> 0.dp - isWidthExpanded -> 24.dp + isLargeWindowSize -> 24.dp else -> 12.dp } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt index 8a1bf23ffd..b820259bef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt @@ -38,12 +38,17 @@ import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import org.signal.core.ui.WindowBreakpoint import org.signal.core.ui.compose.AllDevicePreviews import org.signal.core.ui.compose.Previews +import org.signal.core.ui.getWindowBreakpoint import org.signal.core.ui.isSplitPane +import org.signal.core.ui.isWidthExpanded import org.thoughtcrime.securesms.main.MainFloatingActionButtonsCallback import org.thoughtcrime.securesms.main.MainNavigationBar import org.thoughtcrime.securesms.main.MainNavigationRail @@ -57,14 +62,21 @@ enum class NavigationType { companion object { @Composable fun rememberNavigationType(): NavigationType { - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val resources = LocalResources.current + val config = LocalConfiguration.current + val windowBreakpoint = remember(config) { resources.getWindowBreakpoint() } - return remember(windowSizeClass) { - if (windowSizeClass.isSplitPane()) { - RAIL - } else { - BAR + return when (windowBreakpoint) { + WindowBreakpoint.SMALL -> BAR + WindowBreakpoint.MEDIUM -> { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + if (windowSizeClass.isWidthExpanded) { + RAIL + } else { + BAR + } } + WindowBreakpoint.LARGE -> RAIL } } } @@ -265,7 +277,7 @@ private fun AppScaffoldPreview() { AppScaffold( navigator = rememberAppScaffoldNavigator( - isSplitPane = windowSizeClass.isSplitPane(), + isSplitPane = windowSizeClass.isSplitPane(false), defaultPanePreferredWidth = 416.dp, horizontalPartitionSpacerSize = 16.dp ), diff --git a/core/ui/src/main/java/org/signal/core/ui/WindowSizeClassExtensions.kt b/core/ui/src/main/java/org/signal/core/ui/WindowSizeClassExtensions.kt index 573d828f82..c0911db628 100644 --- a/core/ui/src/main/java/org/signal/core/ui/WindowSizeClassExtensions.kt +++ b/core/ui/src/main/java/org/signal/core/ui/WindowSizeClassExtensions.kt @@ -21,6 +21,9 @@ val WindowSizeClass.isWidthCompact val WindowSizeClass.isHeightCompact get() = !isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND) +val WindowSizeClass.isWidthExpanded + get() = isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND) + fun Resources.getWindowSizeClass(): WindowSizeClass { return WindowSizeClass.BREAKPOINTS_V1.computeWindowSizeClass( widthDp = displayMetrics.widthPixels / displayMetrics.density, @@ -28,6 +31,53 @@ fun Resources.getWindowSizeClass(): WindowSizeClass { ) } +/** + * Determines the device's form factor (PHONE, FOLDABLE, or TABLET) based on the current + * [Resources] and window size class. + * + * This function uses several heuristics: + * - Returns [WindowBreakpoint.SMALL] if the width or height is compact. + * - Returns [WindowBreakpoint.LARGE] if the height is at least the expanded lower bound. + * - Returns [WindowBreakpoint.MEDIUM] if the width is at least the medium lower bound. + * - Otherwise, falls back to aspect ratio heuristics: wider (≥ 1.6) is [WindowBreakpoint.LARGE], else [WindowBreakpoint.MEDIUM]. + * + * @return the inferred [WindowBreakpoint] for the current device. + */ +fun Resources.getWindowBreakpoint(): WindowBreakpoint { + val windowSizeClass = getWindowSizeClass() + + if (windowSizeClass.isWidthCompact || windowSizeClass.isHeightCompact) { + return WindowBreakpoint.SMALL + } + + if (windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_EXPANDED_LOWER_BOUND)) { + return WindowBreakpoint.LARGE + } + + val numerator = maxOf(displayMetrics.widthPixels, displayMetrics.heightPixels) + val denominator = minOf(displayMetrics.widthPixels, displayMetrics.heightPixels) + val aspectRatio = numerator.toFloat() / denominator + + return if (aspectRatio >= 1.6f) { + WindowBreakpoint.LARGE + } else { + WindowBreakpoint.MEDIUM + } +} + +/** + * Indicates the general form factor of the device for responsive UI purposes. + * + * - [SMALL]: A window similar to a phone-sized device, typically with a compact width or height. + * - [MEDIUM]: A window similar to a foldable or medium-size device, or a device which doesn't obviously fit into phone or tablet by heuristics. + * - [LARGE]: A window similar to a large-screen tablet device, typically with an expanded height or wide aspect ratio. + */ +enum class WindowBreakpoint { + SMALL, + MEDIUM, + LARGE +} + /** * Determines whether the UI should display in split-pane mode based on available screen space. */ @@ -40,7 +90,7 @@ fun WindowSizeClass.isSplitPane( } return isAtLeastBreakpoint( - widthDpBreakpoint = WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND, + widthDpBreakpoint = WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND, heightDpBreakpoint = WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND ) } diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/SignalPreviews.kt b/core/ui/src/main/java/org/signal/core/ui/compose/SignalPreviews.kt index b52389e6c8..19e9b0fabc 100644 --- a/core/ui/src/main/java/org/signal/core/ui/compose/SignalPreviews.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/SignalPreviews.kt @@ -19,27 +19,109 @@ annotation class NightPreview() annotation class DayNightPreviews @Preview(name = "phone portrait (day)", uiMode = Configuration.UI_MODE_NIGHT_NO, device = "spec:width=360dp,height=640dp,orientation=portrait") -@Preview(name = "phone portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=360dp,height=640dp,orientation=portrait") +annotation class PhonePortraitDayPreview + @Preview(name = "phone landscape (day)", uiMode = Configuration.UI_MODE_NIGHT_NO, device = "spec:width=640dp,height=360dp,orientation=landscape") +annotation class PhoneLandscapeDayPreview + +@PhonePortraitDayPreview +@PhoneLandscapeDayPreview +annotation class PhoneDayPreviews + +@Preview(name = "phone portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=360dp,height=640dp,orientation=portrait") +annotation class PhonePortraitNightPreview + +@Preview(name = "phone landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=640dp,height=360dp,orientation=landscape") +annotation class PhoneLandscapeNightPreview + +@PhonePortraitNightPreview +@PhoneLandscapeNightPreview +annotation class PhoneNightPreviews + +@PhoneDayPreviews +@PhoneLandscapeNightPreview annotation class PhonePreviews -@Preview(name = "foldable portrait (day)", uiMode = Configuration.UI_MODE_NIGHT_NO, device = "spec:width=600dp,height=1024dp,orientation=portrait") -@Preview(name = "foldable landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=1024dp,height=600dp,orientation=landscape") +@Preview(name = "small foldable portrait (day)", uiMode = Configuration.UI_MODE_NIGHT_NO, device = "spec:width=620dp,height=720dp,orientation=portrait") +annotation class SmallFoldablePortraitDayPreview + +@Preview(name = "small foldable landscape (day)", uiMode = Configuration.UI_MODE_NIGHT_NO, device = "spec:width=720dp,height=620dp,orientation=landscape") +annotation class SmallFoldableLandscapeDayPreview + +@SmallFoldablePortraitDayPreview +@SmallFoldableLandscapeDayPreview +annotation class SmallFoldableDayPreviews + +@Preview(name = "small foldable portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=620dp,height=720dp,orientation=portrait") +annotation class SmallFoldablePortraitNightPreview + +@Preview(name = "small foldable landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=720dp,height=620dp,orientation=landscape") +annotation class SmallFoldableLandscapeNightPreview + +@SmallFoldablePortraitNightPreview +@SmallFoldableLandscapeNightPreview +annotation class SmallFoldableNightPreviews + +@SmallFoldableDayPreviews +@SmallFoldableLandscapeNightPreview +annotation class SmallFoldablePreviews + +@Preview(name = "foldable portrait (day)", uiMode = Configuration.UI_MODE_NIGHT_NO, device = "spec:width=850dp,height=881dp,orientation=portrait") +annotation class FoldablePortraitDayPreview + +@Preview(name = "foldable landscape (day)", uiMode = Configuration.UI_MODE_NIGHT_NO, device = "spec:width=881dp,height=850dp,orientation=landscape") +annotation class FoldableLandscapeDayPreview + +@FoldablePortraitDayPreview +@FoldableLandscapeDayPreview +annotation class FoldableDayPreviews + +@Preview(name = "foldable portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=850dp,height=881dp,orientation=portrait") +annotation class FoldablePortraitNightPreview + +@Preview(name = "foldable landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=881dp,height=850dp,orientation=landscape") +annotation class FoldableLandscapeNightPreview + +@FoldablePortraitNightPreview +@FoldableLandscapeNightPreview +annotation class FoldableNightPreviews + +@FoldableDayPreviews +@FoldableLandscapeNightPreview annotation class FoldablePreviews -@Preview(name = "tablet portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=840dp,height=1280dp,orientation=portrait") +@Preview(name = "tablet portrait (day)", uiMode = Configuration.UI_MODE_NIGHT_NO, device = "spec:width=840dp,height=1280dp,orientation=portrait") +annotation class TabletPortraitDayPreview + @Preview(name = "tablet landscape (day)", uiMode = Configuration.UI_MODE_NIGHT_NO, device = "spec:width=1280dp,height=840dp,orientation=landscape") +annotation class TabletLandscapeDayPreview + +@TabletPortraitDayPreview +@TabletLandscapeDayPreview +annotation class TabletDayPreviews + +@Preview(name = "tablet portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=1280dp,height=840dp,orientation=portrait") +annotation class TabletPortraitNightPreview + +@Preview(name = "tablet landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=840dp,height=1280dp,orientation=landscape") +annotation class TabletLandscapeNightPreview + +@TabletPortraitNightPreview +@TabletLandscapeNightPreview +annotation class TabletNightPreviews + +@TabletDayPreviews +@TabletLandscapeNightPreview annotation class TabletPreviews -@Preview(name = "phone portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=360dp,height=640dp,orientation=portrait") -@Preview(name = "phone landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=640dp,height=360dp,orientation=landscape") -@Preview(name = "foldable portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=600dp,height=1024dp,orientation=portrait") -@Preview(name = "foldable landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=1024dp,height=600dp,orientation=landscape") -@Preview(name = "tablet portrait (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=840dp,height=1280dp,orientation=portrait") -@Preview(name = "tablet landscape (night)", uiMode = Configuration.UI_MODE_NIGHT_YES, device = "spec:width=1280dp,height=840dp,orientation=landscape") +@PhoneNightPreviews +@SmallFoldableNightPreviews +@FoldableNightPreviews +@TabletNightPreviews annotation class AllNightPreviews @PhonePreviews +@SmallFoldablePreviews @FoldablePreviews @TabletPreviews annotation class AllDevicePreviews