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

Commit 831c5d88 authored by Qijing Yao's avatar Qijing Yao Committed by Android (Google) Code Review
Browse files

Merge "Shrink oversize windows after dragged to a smaller display" into main

parents 0c77c5e1 5d093a0c
Loading
Loading
Loading
Loading
+112 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.wm.shell.common
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import android.util.MathUtils.min

/**
 * Utility class for calculating bounds during multi-display drag operations.
@@ -26,6 +27,8 @@ import android.graphics.RectF
 * This class provides helper functions to perform bounds calculation during window drag.
 */
object MultiDisplayDragMoveBoundsCalculator {
    private const val OVERHANG_DP = 96

    /**
     * Calculates the global DP bounds of a window being dragged across displays.
     *
@@ -67,6 +70,115 @@ object MultiDisplayDragMoveBoundsCalculator {
        return RectF(currentLeftDp, currentTopDp, currentRightDp, currentBottomDp)
    }

    /**
     * Adjusts window bounds to fit within the visible area of a display, including a small
     * "overhang" margin.
     *
     * For resizable windows, the bounds are simply intersected with the overhang region. For
     * non-resizable windows, it scales the window down, preserving its aspect ratio, to fit within
     * the allowed area.
     *
     * @param originalBounds The window's current bounds in screen pixel coordinates.
     * @param displayLayout The layout of the display where the bounds need to be constrained.
     * @param isResizeable True if the window can be resized, false otherwise.
     * @param pointerX The pointer's horizontal position in the display's local pixel coordinates,
     *   used as a scaling anchor.
     * @return The adjusted bounds in screen pixel coordinates.
     */
    @JvmStatic
    fun constrainBoundsForDisplay(
        originalBounds: Rect,
        displayLayout: DisplayLayout?,
        isResizeable: Boolean,
        pointerX: Float,
    ): Rect {
        if (displayLayout == null) {
            return originalBounds
        }

        // Define the allowed screen area, including a small overhang margin.
        val displayBoundsOverhang = Rect(0, 0, displayLayout.width(), displayLayout.height())
        val overhang = displayLayout.dpToPx(OVERHANG_DP).toInt()
        displayBoundsOverhang.inset(-overhang, -overhang)

        if (displayBoundsOverhang.contains(originalBounds)) {
            return originalBounds
        }

        val intersectBounds = Rect()
        intersectBounds.setIntersect(displayBoundsOverhang, originalBounds)
        // For resizable windows, we employ a logic similar to window trimming.
        if (isResizeable) {
            return Rect(intersectBounds)
        }

        // For non-resizable windows, scale the window down to make sure all edges within overhang.
        if (
            originalBounds.width() <= 0 ||
                originalBounds.height() <= 0 ||
                intersectBounds.width() <= 0 ||
                intersectBounds.height() <= 0
        ) {
            return intersectBounds
        }

        val scaleFactorHorizontal = intersectBounds.width().toFloat() / originalBounds.width()
        val scaleFactorVertical = intersectBounds.height().toFloat() / originalBounds.height()
        val scaleFactor = min(scaleFactorHorizontal, scaleFactorVertical)

        val isLeftCornerIn = displayBoundsOverhang.contains(originalBounds.left, originalBounds.top)
        val isRightCornerIn =
            displayBoundsOverhang.contains(originalBounds.right, originalBounds.top)
        if (isLeftCornerIn && isRightCornerIn) {
            // Case 1: Both top corners are on-screen. Anchor to the pointer's horizontal position.
            return scaleWithHorizontalOrigin(originalBounds, scaleFactor, pointerX)
        } else if (isLeftCornerIn) {
            // Case 2: Only the top-left corner is on-screen. Anchor to that corner.
            return scaleWithHorizontalOrigin(
                originalBounds,
                scaleFactor,
                originalBounds.left.toFloat(),
            )
        } else if (isRightCornerIn) {
            // Case 3: Only the top-right corner is on-screen. Anchor to that corner.
            return scaleWithHorizontalOrigin(
                originalBounds,
                scaleFactor,
                originalBounds.right.toFloat(),
            )
        }

        // Case 4: Both top corners are off-screen.
        if (scaleFactorHorizontal > scaleFactorVertical) {
            // The height is the limiting factor. We can still safely anchor to the pointer's
            // horizontal position while scaling to fit vertically.
            return scaleWithHorizontalOrigin(originalBounds, scaleFactorVertical, pointerX)
        }
        // The width is the limiting factor. To prevent anchoring to a potentially far-off-screen
        // point, we force the window's width to match the allowed display width, and then scales
        // the height proportionally to maintain the aspect ratio.
        return Rect(
            displayBoundsOverhang.left,
            originalBounds.top,
            displayBoundsOverhang.right,
            originalBounds.top + (originalBounds.height() * scaleFactorHorizontal).toInt(),
        )
    }

    /**
     * Scales a Rect from a horizontal anchor point, keeping the top edge fixed.
     */
    private fun scaleWithHorizontalOrigin(
        originalBounds: Rect,
        scaleFactor: Float,
        originX: Float,
    ): Rect {
        val height = (originalBounds.height() * scaleFactor).toInt()
        val left = (originX + (originalBounds.left - originX) * scaleFactor).toInt()
        val right = (originX + (originalBounds.right - originX) * scaleFactor).toInt()
        return Rect(left, originalBounds.top, right, originalBounds.top + height)
    }

    /**
     * Converts global DP bounds to local pixel bounds for a specific display.
     *
+16 −1
Original line number Diff line number Diff line
@@ -101,6 +101,7 @@ import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.common.ExternalInterfaceBinder
import com.android.wm.shell.common.HomeIntentProvider
import com.android.wm.shell.common.MultiDisplayDragMoveBoundsCalculator
import com.android.wm.shell.common.MultiInstanceHelper
import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent
import com.android.wm.shell.common.RemoteCallable
@@ -4670,10 +4671,24 @@ class DesktopTasksController(
                        displayAreaInfo != null

                if (isCrossDisplayDrag) {
                    val constrainedBounds =
                        if (
                            DesktopExperienceFlags.ENABLE_SHRINK_WINDOW_BOUNDS_AFTER_DRAG.isTrue()
                        ) {
                            MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay(
                                destinationBounds,
                                displayController.getDisplayLayout(newDisplayId),
                                taskInfo.isResizeable,
                                inputCoordinate.x,
                            )
                        } else {
                            Rect(destinationBounds)
                        }

                    moveToDisplay(
                        taskInfo,
                        newDisplayId,
                        destinationBounds,
                        constrainedBounds,
                        dragToDisplayTransitionHandler,
                    )
                } else {
+150 −1
Original line number Diff line number Diff line
@@ -13,7 +13,6 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.wm.shell.common

import android.content.res.Configuration
@@ -81,4 +80,154 @@ class MultiDisplayDragMoveBoundsCalculatorTest : ShellTestCase() {
        val expectedBoundsPx = Rect(100, 1300, 400, 1500)
        assertEquals(expectedBoundsPx, actualBoundsPx)
    }

    @Test
    fun constrainBoundsForDisplay_nullDisplayLayout_returnsOriginalBounds() {
        val originalBounds = Rect(10, 20, 110, 120)
        val result =
            MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay(
                originalBounds,
                displayLayout = null,
                isResizeable = false,
                pointerX = 50f,
            )
        assertEquals(originalBounds, result)
    }

    @Test
    fun constrainBoundsForDisplay_fullyContained_returnsOriginalBounds() {
        // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324)
        val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources)
        // Fits well within DISPLAY + overhang
        val originalBounds = Rect(10, 20, 110, 120)

        val result =
            MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay(
                originalBounds,
                displayLayout,
                isResizeable = true,
                pointerX = 50f,
            )

        assertEquals(originalBounds, result)
    }

    @Test
    fun constrainBoundsForDisplay_resizableExceedsOverhang_returnsIntersection() {
        // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324)
        val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources)
        // Window bounds exceeds right and bottom of overhang
        val originalBounds = Rect(10, 20, 250, 400)

        val result =
            MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay(
                originalBounds,
                displayLayout,
                isResizeable = true,
                pointerX = 10f,
            )

        val expectedBounds = Rect(10, 20, 124, 324)
        assertEquals(expectedBounds, result)
    }

    @Test
    fun constrainBoundsForDisplay_nonResizableTopEdgeContained_scaleBasedOnPointer() {
        // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324)
        val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources)
        // width=50, height=100, exceeding bottom.
        val originalBounds = Rect(10, 254, 60, 354)

        val result =
            MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay(
                originalBounds,
                displayLayout,
                isResizeable = false,
                pointerX = 40f,
            )

        // width=35, height=70
        val expectedBounds = Rect(19, 254, 54, 324)
        assertEquals(expectedBounds, result)
    }

    @Test
    fun constrainBoundsForDisplay_nonResizableTopLeftCornerInside_scaleBasedOnTopLeftCorner() {
        // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324)
        val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources)
        // width=88, height=100, exceeding right.
        val originalBounds = Rect(80, 100, 168, 200)

        val result =
            MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay(
                originalBounds,
                displayLayout,
                isResizeable = false,
                pointerX = 100f,
            )

        // width=44, height=50
        val expectedBounds = Rect(80, 100, 124, 150)
        assertEquals(expectedBounds, result)
    }

    @Test
    fun constrainBoundsForDisplay_nonResizableTopRightCornerInside_scaleBasedOnTopRightCorner() {
        // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324)
        val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources)
        // width=106, height=200, exceeding left and bottom.
        val originalBounds = Rect(-30, 224, 76, 424)

        val result =
            MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay(
                originalBounds,
                displayLayout,
                isResizeable = false,
                pointerX = 100f,
            )

        // width=53, height=100
        val expectedBounds = Rect(23, 224, 76, 324)
        assertEquals(expectedBounds, result)
    }

    @Test
    fun constrainBoundsForDisplay_nonResizableBothTopCornersOutsideHeightLimited_scaleBasedOnPointer() {
        // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324)
        val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources)
        // width=180, height=200, exceeding left, right and bottom.
        val originalBounds = Rect(-30, 224, 150, 424)

        val result =
            MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay(
                originalBounds,
                displayLayout,
                isResizeable = false,
                pointerX = 40f,
            )

        // width=90, height=100
        val expectedBounds = Rect(5, 224, 95, 324)
        assertEquals(expectedBounds, result)
    }

    @Test
    fun constrainBoundsForDisplay_nonResizableBothTopCornersOutsideWidthLimited_scaleToFitDisplay() {
        // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324)
        val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources)
        // width=296, height=100, exceeding left, right and bottom.
        val originalBounds = Rect(-30, 54, 266, 154)

        val result =
            MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay(
                originalBounds,
                displayLayout,
                isResizeable = false,
                pointerX = 40f,
            )

        // width=148, height=50
        val expectedBounds = Rect(-24, 54, 124, 104)
        assertEquals(expectedBounds, result)
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -42,6 +42,10 @@ object MultiDisplayTestUtil {
        fun getSpyDisplayLayout(resources: Resources): DisplayLayout {
            val displayInfo = DisplayInfo()
            displayInfo.logicalDensityDpi = dpi
            displayInfo.logicalWidth =
                (bounds.width() * dpi / DisplayMetrics.DENSITY_DEFAULT).toInt()
            displayInfo.logicalHeight =
                (bounds.height() * dpi / DisplayMetrics.DENSITY_DEFAULT).toInt()
            val displayLayout = spy(DisplayLayout(displayInfo, resources, true, true))
            displayLayout.setGlobalBoundsDp(bounds)
            return displayLayout