Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 79531d99 authored by Ioana Alexandru's avatar Ioana Alexandru
Browse files

[Compose Notifs] Make PrioritizedRow handle same importance

Children with the same importance should shrink together.

Bug: 431222735
Test: PrioritizedRowTest
Test: manual in the Gallery app
Flag: EXEMPT not production code yet
Change-Id: I46f5d4b1f212842c42906c779fef0294e78aae6f
parent 613d9a5e
Loading
Loading
Loading
Loading
+99 −35
Original line number Diff line number Diff line
@@ -136,6 +136,10 @@ public fun PrioritizedRow(
        init {
            check(hideWidth <= reducedWidth) { "hideWidth must be smaller than reducedWidth" }
        }

        fun shrinkable(): Boolean = !isSeparator && (reducedWidth < currentWidth)

        fun hideable(): Boolean = canHide && isVisible && (hideWidth < currentWidth)
    }

    fun List<LayoutCandidate>.previousVisibleChild(index: Int): LayoutCandidate? {
@@ -199,33 +203,89 @@ public fun PrioritizedRow(

            // SHRINK: The content doesn't fit, start shrinking elements down to their reduced width
            // based on priority
            for (i in sortedContent.indices) {
                val shrinkCandidate = sortedContent[i]
            var i = 0
            while (i < sortedContent.size) {
                if (!sortedContent[i].shrinkable()) {
                    i++
                    continue
                }

                // Take all candidates with the same importance, and shrink them simultaneously
                val importance = sortedContent[i].importance
                var shrinkableCnt = 1
                var lastShrinkableIdx = i
                for (j in i + 1 until sortedContent.size) {
                    if (sortedContent[j].importance != importance) break
                    if (sortedContent[j].shrinkable()) {
                        lastShrinkableIdx = j
                        shrinkableCnt++
                    }
                }

                var remainingShrinkables = shrinkableCnt
                for (j in i..lastShrinkableIdx) {
                    val shrinkCandidate = sortedContent[j]
                    if (!shrinkCandidate.shrinkable()) continue

                    // Distribute the space needed across all remaining candidates
                    val wantedSpace =
                        (overflow / remainingShrinkables) +
                            minOf(1, overflow % remainingShrinkables)
                    remainingShrinkables--

                // TODO: b/431222735 - Shrink elements with the same importance simultaneously.
                val shrinkableSpace = shrinkCandidate.currentWidth - shrinkCandidate.reducedWidth
                if (shrinkableSpace <= 0) continue
                val shrinkAmount = minOf(overflow, shrinkableSpace)
                    val shrinkableSpace =
                        shrinkCandidate.currentWidth - shrinkCandidate.reducedWidth
                    val shrinkAmount = minOf(wantedSpace, shrinkableSpace, overflow)
                    shrinkCandidate.currentWidth -= shrinkAmount

                    overflow -= shrinkAmount
                }

                if (overflow <= 0) break
                i = lastShrinkableIdx + 1
            }

            // HIDE: Content still doesn't fit, so we need to shrink elements further, and maybe
            // even hide them.
            var somethingWasHidden = false
            if (overflow > 0) {
                for (i in sortedContent.indices) {
                    val hideCandidate = sortedContent[i]
                    if (!hideCandidate.canHide || !hideCandidate.isVisible) continue
                i = 0
                while (i < sortedContent.size) {
                    if (!sortedContent[i].hideable()) {
                        i++
                        continue
                    }

                    // Take all hideable candidates with the same importance, and try to shrink them
                    // proportionally before hiding them
                    val importance = sortedContent[i].importance
                    var hideableCnt = 1
                    var lastHideableIdx = i
                    for (j in i + 1 until sortedContent.size) {
                        if (sortedContent[j].importance != importance) break
                        if (sortedContent[j].hideable()) {
                            lastHideableIdx = j
                            hideableCnt++
                        }
                    }

                    var remainingHideables = hideableCnt
                    for (j in i..lastHideableIdx) {
                        val hideCandidate = sortedContent[j]
                        if (!hideCandidate.hideable()) continue

                        // Distribute the space needed across all remaining candidates
                        val wantedSpace =
                            (overflow / remainingHideables) +
                                minOf(1, overflow % remainingHideables)
                        remainingHideables--

                        // One last attempt to shrink this element further
                        val shrinkableSpace = hideCandidate.currentWidth - hideCandidate.hideWidth
                    if (shrinkableSpace >= overflow) {
                        hideCandidate.currentWidth -= overflow
                        overflow = 0
                        break
                        if (shrinkableSpace >= wantedSpace) {
                            hideCandidate.currentWidth -= wantedSpace
                            overflow -= wantedSpace
                            continue
                        }

                        // Shrinking wouldn't be enough, so let's hide it
@@ -250,6 +310,10 @@ public fun PrioritizedRow(
                        overflow -= spaceToReclaim
                        if (overflow <= 0) break
                    }

                    if (overflow <= 0) break
                    i = lastHideableIdx + 1
                }
            }

            // REGROW: If hiding items created extra space, give it back to visible shrunk items.
+2 −2
Original line number Diff line number Diff line
@@ -90,7 +90,7 @@ internal fun TopLineText(

        if (title != null) {
            isFirstElement = false
            Title(title, Modifier.shrinkable(importance = 5, minWidth = reducedWidth))
            Title(title, Modifier.shrinkable(importance = 3, minWidth = reducedWidth))
        }
        if (appNameText != null) {
            maybeAddSeparator()
@@ -105,7 +105,7 @@ internal fun TopLineText(
                text = headerTextSecondary,
                modifier =
                    Modifier.hideable(
                        importance = 4,
                        importance = 3,
                        reducedWidth = reducedWidth,
                        hideWidth = hideWidth,
                    ),
+48 −39
Original line number Diff line number Diff line
@@ -95,6 +95,36 @@ class PrioritizedRowTest : SysuiTestCase() {
        rule.onNodeWithTag("row").assertWidthIsEqualTo(420.dp)
    }

    @Test
    fun width320dp_sameImportance_allShrinkablesShrink() {
        rule.setContent {
            CompositionLocalProvider(LocalDensity provides density) {
                PlatformTheme {
                    PrioritizedRow(modifier = Modifier.width(320.dp).testTag("row")) {
                        // All children have the same importance (everything else stays the same)
                        TestContent(forceSameImportance = true)
                    }
                }
            }
        }

        rule.onNodeWithTag("icon0").assertIsDisplayedWithWidth(iconWidth)
        rule.onNodeWithTag("spacer0").assertIsDisplayedWithWidth(separatorWidth)
        rule.onNodeWithTag("icon1").assertIsDisplayedWithWidth(iconWidth)
        rule.onNodeWithTag("spacer1").assertIsDisplayedWithWidth(separatorWidth)
        // The required space is distributed across all 3 shrinkables, but they can't go below
        // reducedWidth.
        rule.onNodeWithText("High Importance").assertIsDisplayedWithWidth(reducedWidth)
        rule.onNodeWithTag("dot0").assertIsDisplayedWithWidth(separatorWidth)
        rule.onNodeWithText("Medium (Shrinkable)").assertIsDisplayedWithWidth(57.5.dp)
        rule.onNodeWithTag("dot1").assertIsDisplayedWithWidth(separatorWidth)
        rule.onNodeWithTag("Low (Hideable)").assertIsDisplayedWithWidth(64.5.dp)
        rule.onNodeWithTag("spacer2").assertIsDisplayedWithWidth(separatorWidth)
        rule.onNodeWithText("FIXED").assertIsDisplayedWithWidth(fixedWidth)

        rule.onNodeWithTag("row").assertWidthIsEqualTo(320.dp)
    }

    @Test
    fun width400dp_lowPriorityRowShrinks() {
        rule.setContent {
@@ -183,21 +213,21 @@ class PrioritizedRowTest : SysuiTestCase() {
    }

    @Test
    fun width290dp_firstIconShrinks() {
    fun width280dp_startIconsShrink() {
        rule.setContent {
            CompositionLocalProvider(LocalDensity provides density) {
                PlatformTheme {
                    PrioritizedRow(modifier = Modifier.width(290.dp).testTag("row")) {
                    PrioritizedRow(modifier = Modifier.width(280.dp).testTag("row")) {
                        TestContent()
                    }
                }
            }
        }

        // First icon starts shrinking before being hidden
        rule.onNodeWithTag("icon0").assertIsDisplayedWithWidth(16.dp)
        // Start icons have the same importance, so they both start shrinking
        rule.onNodeWithTag("icon0").assertIsDisplayedWithWidth(15.dp)
        rule.onNodeWithTag("spacer0").assertIsDisplayedWithWidth(separatorWidth)
        rule.onNodeWithTag("icon1").assertIsDisplayedWithWidth(iconWidth)
        rule.onNodeWithTag("icon1").assertIsDisplayedWithWidth(15.dp)
        rule.onNodeWithTag("spacer1").assertIsDisplayedWithWidth(separatorWidth)
        // High priority text doesn't shrink beyond reducedWidth
        rule.onNodeWithText("High Importance").assertIsDisplayedWithWidth(reducedWidth)
@@ -209,36 +239,7 @@ class PrioritizedRowTest : SysuiTestCase() {
        rule.onNodeWithTag("spacer2").assertIsDisplayedWithWidth(separatorWidth)
        rule.onNodeWithText("FIXED").assertIsDisplayedWithWidth(fixedWidth)

        rule.onNodeWithTag("row").assertWidthIsEqualTo(290.dp)
    }

    @Test
    fun width240dp_firstIconHides() {
        rule.setContent {
            CompositionLocalProvider(LocalDensity provides density) {
                PlatformTheme {
                    PrioritizedRow(modifier = Modifier.width(240.dp).testTag("row")) {
                        TestContent()
                    }
                }
            }
        }

        // First icon and corresponding spacer disappear
        rule.onNodeWithTag("icon0").assertIsNotDisplayed()
        rule.onNodeWithTag("spacer0").assertIsNotDisplayed()
        // Second icon starts shrinking
        rule.onNodeWithTag("icon1").assertIsDisplayedWithWidth(5.dp)
        rule.onNodeWithTag("spacer1").assertIsDisplayedWithWidth(separatorWidth)
        rule.onNodeWithText("High Importance").assertIsDisplayedWithWidth(reducedWidth)
        rule.onNodeWithTag("dot0").assertIsDisplayedWithWidth(separatorWidth)
        rule.onNodeWithText("Medium (Shrinkable)").assertIsDisplayedWithWidth(reducedWidth)
        rule.onNodeWithTag("dot1").assertIsDisplayedWithWidth(separatorWidth)
        rule.onNodeWithTag("Low (Hideable)").assertIsDisplayedWithWidth(reducedWidth)
        rule.onNodeWithTag("spacer2").assertIsDisplayedWithWidth(separatorWidth)
        rule.onNodeWithText("FIXED").assertIsDisplayedWithWidth(fixedWidth)

        rule.onNodeWithTag("row").assertWidthIsEqualTo(240.dp)
        rule.onNodeWithTag("row").assertWidthIsEqualTo(280.dp)
    }

    @Test
@@ -329,7 +330,7 @@ class PrioritizedRowTest : SysuiTestCase() {

    // Note: This composable is forked in the Compose Gallery app for interactive, manual testing.
    @Composable
    private fun PrioritizedRowScope.TestContent() {
    private fun PrioritizedRowScope.TestContent(forceSameImportance: Boolean = false) {
        // Note: This font family & size  (together with the fixed density configuration) means that
        // each character in a text composable will have a width of 5dp.
        val fontFamily = FontFamily.Monospace
@@ -355,7 +356,11 @@ class PrioritizedRowTest : SysuiTestCase() {
        // This text will be the last to shrink
        Text(
            text = "High Importance",
            modifier = Modifier.shrinkable(importance = 3, minWidth = reducedWidth),
            modifier =
                Modifier.shrinkable(
                    importance = if (forceSameImportance) 0 else 3,
                    minWidth = reducedWidth,
                ),
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
            fontFamily = fontFamily,
@@ -371,7 +376,11 @@ class PrioritizedRowTest : SysuiTestCase() {
        // This text will shrink to its minWidth
        Text(
            text = "Medium (Shrinkable)",
            modifier = Modifier.shrinkable(importance = 2, minWidth = reducedWidth),
            modifier =
                Modifier.shrinkable(
                    importance = if (forceSameImportance) 0 else 2,
                    minWidth = reducedWidth,
                ),
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
            fontFamily = fontFamily,
@@ -388,7 +397,7 @@ class PrioritizedRowTest : SysuiTestCase() {
        Row(
            modifier =
                Modifier.hideable(
                        importance = 1,
                        importance = if (forceSameImportance) 0 else 1,
                        reducedWidth = reducedWidth,
                        hideWidth = hideWidth,
                    )