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

Commit 7039007b authored by Nikki Moteva's avatar Nikki Moteva
Browse files

System UI: Enable resizing edge for screen capture's region select box.

This change introduces drag to resize for the edges. We now have
ResizeZone, which is an interface that is one of Corner or Edge. For any
drag event, we will check whether a corner has been dragged first, and
then we check if an edge has been dragged.

Bug: 417533326
Test: Manual
Flag: com.android.systemui.desktop_screen_capture
Change-Id: Ia7b6d9cd4d6eeb949c3355c022d03650acfd07e5
parent f6ca06d5
Loading
Loading
Loading
Loading
+99 −59
Original line number Diff line number Diff line
@@ -43,45 +43,70 @@ import androidx.compose.ui.unit.dp
import com.android.systemui.common.shared.model.Icon as IconModel
import com.android.systemui.res.R

/**
 * A common interface for all draggable zones of the resizable box. The ResizeZone is one of Corner
 * or Edge.
 */
sealed interface ResizeZone

/**
 * An enum to identify each of the four corners of the rectangle.
 *
 * @param alignment The alignment of the corner within the box.
 */
enum class Corner(val alignment: Alignment) {
enum class Corner(val alignment: Alignment) : ResizeZone {
    TopLeft(Alignment.TopStart),
    TopRight(Alignment.TopEnd),
    BottomLeft(Alignment.BottomStart),
    BottomRight(Alignment.BottomEnd),
}

/** An enum to identify each of the four edges of the rectangle. */
enum class Edge : ResizeZone {
    Top,
    Bottom,
    Left,
    Right,
}

/**
 * Determines which corner of a box is being touched based on the press offset.
 * Determines which zone (corner or edge) of a box is being touched based on the press offset.
 *
 * @param boxWidth The total width of the box.
 * @param boxHeight The total height of the box.
 * @param startOffset The position of the initial press.
 * @param touchAreaPx The size of the touch area in pixels.
 * @return The Corner that was touched, or `null` if the press was not in a corner.
 * @return The ResizeZone that was pressed, or `null` if the press was not on a zone.
 */
private fun getDraggedCorner(
private fun getDragZone(
    boxWidth: Float,
    boxHeight: Float,
    startOffset: Offset,
    touchAreaPx: Float,
): Corner? {
): ResizeZone? {
    val isTouchingTop = startOffset.y in -touchAreaPx..touchAreaPx
    val isTouchingBottom = startOffset.y in (boxHeight - touchAreaPx)..boxHeight
    val isTouchingLeft = startOffset.x in -touchAreaPx..touchAreaPx
    val isTouchingRight = startOffset.x in (boxWidth - touchAreaPx)..boxWidth

    return when {
        isTouchingTop && isTouchingLeft -> Corner.TopLeft
        isTouchingTop && isTouchingRight -> Corner.TopRight
        isTouchingBottom && isTouchingLeft -> Corner.BottomLeft
        isTouchingBottom && isTouchingRight -> Corner.BottomRight
        else -> null
    // Corners should be checked first because edges are also touched when the user touches a
    // corner.
    when {
        isTouchingTop && isTouchingLeft -> return Corner.TopLeft
        isTouchingTop && isTouchingRight -> return Corner.TopRight
        isTouchingBottom && isTouchingLeft -> return Corner.BottomLeft
        isTouchingBottom && isTouchingRight -> return Corner.BottomRight
    }

    // Edges are checked next.
    when {
        isTouchingTop -> return Edge.Top
        isTouchingBottom -> return Edge.Bottom
        isTouchingLeft -> return Edge.Left
        isTouchingRight -> return Edge.Right
    }

    return null
}

/**
@@ -119,10 +144,9 @@ fun RegionBox(
        )
    }

    val onCornerDrag:
        (dragAmount: Offset, corner: Corner, maxWidth: Float, maxHeight: Float) -> Unit =
        { dragAmount, corner, maxWidth, maxHeight ->
            // Used for calculating the new dimensions based on which corner is dragged.
    val onResizeDrag:
        (dragAmount: Offset, zone: ResizeZone, maxWidth: Float, maxHeight: Float) -> Unit =
        { dragAmount, zone, maxWidth, maxHeight ->
            var newLeft = rect.left
            var newTop = rect.top
            var newRight = rect.right
@@ -131,30 +155,50 @@ fun RegionBox(
            val (dragX, dragY) = dragAmount

            // Handle horizontal drag for resizing.
            if (corner == Corner.TopLeft || corner == Corner.BottomLeft) {
            when (zone) {
                Corner.TopLeft,
                Corner.BottomLeft,
                Edge.Left -> {
                    val potentialNewLeft = rect.left + dragX
                    val rightLimitForMinWidth = rect.right - minSizePx

                    newLeft = potentialNewLeft.coerceIn(0f, rightLimitForMinWidth)
            } else {
                }
                Corner.TopRight,
                Corner.BottomRight,
                Edge.Right -> {
                    val potentialNewRight = rect.right + dragX
                    val leftLimitForMinWidth = rect.left + minSizePx

                    newRight = potentialNewRight.coerceIn(leftLimitForMinWidth, maxWidth)
                }
                else -> {
                    // No horizontal change for Top or Bottom edges.
                }
            }

            // Handle vertical drag for resizing.
            if (corner == Corner.TopLeft || corner == Corner.TopRight) {
            when (zone) {
                Corner.TopLeft,
                Corner.TopRight,
                Edge.Top -> {
                    val potentialNewTop = rect.top + dragY
                    val bottomLimitForMinHeight = rect.bottom - minSizePx

                    newTop = potentialNewTop.coerceIn(0f, bottomLimitForMinHeight)
            } else {
                }
                Corner.BottomLeft,
                Corner.BottomRight,
                Edge.Bottom -> {
                    val potentialNewBottom = rect.bottom + dragY
                    val topLimitForMinHeight = rect.top + minSizePx

                    newBottom = potentialNewBottom.coerceIn(topLimitForMinHeight, maxHeight)
                }
                else -> {
                    // No vertical change for Left or Right edges.
                }
            }

            rect = Rect(newLeft, newTop, newRight, newBottom)
        }
@@ -176,7 +220,7 @@ fun RegionBox(

    ResizableRectangle(
        rect = rect,
        onCornerDrag = onCornerDrag,
        onResizeDrag = onResizeDrag,
        onBoxDrag = onBoxDrag,
        onDragEnd = {
            onDragEnd(
@@ -190,10 +234,11 @@ fun RegionBox(
}

/**
 * A box with a border that can be resized by dragging its corners and moved by dragging its body.
 * A box with a border that can be resized by dragging its zone (corner or edge), and moved by
 * dragging its body.
 *
 * @param rect The current geometry of the region box.
 * @param onCornerDrag Callback invoked when a corner is dragged.
 * @param onResizeDrag Callback invoked when a corner or edge is dragged.
 * @param onBoxDrag Callback invoked when the main body of the box is dragged.
 * @param onDragEnd Callback invoked when a drag gesture finishes.
 * @param modifier The modifier to be applied to the composable.
@@ -201,7 +246,7 @@ fun RegionBox(
@Composable
private fun ResizableRectangle(
    rect: Rect,
    onCornerDrag: (dragAmount: Offset, corner: Corner, maxWidth: Float, maxHeight: Float) -> Unit,
    onResizeDrag: (dragAmount: Offset, zone: ResizeZone, maxWidth: Float, maxHeight: Float) -> Unit,
    onBoxDrag: (dragAmount: Offset, maxWidth: Float, maxHeight: Float) -> Unit,
    onDragEnd: () -> Unit,
    modifier: Modifier = Modifier,
@@ -213,18 +258,18 @@ private fun ResizableRectangle(

    // The width of the border stroke around the region box.
    val borderStrokeWidth = 4.dp
    // The touch area for detecting a corner resize drag.
    val cornerTouchArea = 48.dp
    // The touch area for detecting an edge or corner resize drag.
    val touchArea = 48.dp

    // Must remember the screen size for the drag logic. Initial values are set to 0.
    var screenWidth by remember { mutableStateOf(0f) }
    var screenHeight by remember { mutableStateOf(0f) }

    val density = LocalDensity.current
    val cornerTouchAreaPx = with(density) { cornerTouchArea.toPx() }
    val touchAreaPx = with(density) { touchArea.toPx() }

    // State of the corner, can be either dragged or null.
    var draggedCorner by remember { mutableStateOf<Corner?>(null) }
    // The zone being dragged for resizing, if any.
    var draggedZone by remember { mutableStateOf<ResizeZone?>(null) }

    Box(
        modifier =
@@ -245,40 +290,35 @@ private fun ResizableRectangle(
                        height = with(density) { rect.height.toDp() },
                    )
                    .border(borderStrokeWidth, MaterialTheme.colorScheme.onSurfaceVariant)
                    .pointerInput(screenWidth, screenHeight, onCornerDrag, onBoxDrag, onDragEnd) {
                    .pointerInput(screenWidth, screenHeight, onResizeDrag, onBoxDrag, onDragEnd) {
                        detectDragGestures(
                            onDragStart = { startOffset ->
                                draggedCorner =
                                    getDraggedCorner(
                                draggedZone =
                                    getDragZone(
                                        boxWidth = size.width.toFloat(),
                                        boxHeight = size.height.toFloat(),
                                        startOffset = startOffset,
                                        touchAreaPx = cornerTouchAreaPx,
                                        touchAreaPx = touchAreaPx,
                                    )
                            },
                            onDragEnd = {
                                draggedCorner = null
                                draggedZone = null
                                onDragEnd()
                            },
                            onDrag = { change, dragAmount ->
                                change.consume()

                                // Create a stable and local copy of the dragged corner. This
                                // ensures that the value does not change in the onCornerDrag
                                // Create a stable and local copy of the draggedZone. This
                                // ensures that the value does not change in the onResizeDrag
                                // callback.
                                val currentCorner = draggedCorner

                                if (currentCorner != null) {
                                    // If 'currentCorner' has a value, it means we are dragging a
                                    // corner for resizing.
                                    onCornerDrag(
                                        dragAmount,
                                        currentCorner,
                                        screenWidth,
                                        screenHeight,
                                    )
                                val currentZone = draggedZone

                                if (currentZone != null) {
                                    // If currentZone has a value, it means we are dragging a zone
                                    // for resizing.
                                    onResizeDrag(dragAmount, currentZone, screenWidth, screenHeight)
                                } else {
                                    // If 'currentCorner' is null, it means we are dragging the box.
                                    // If currentZone is null, it means we are dragging the box.
                                    onBoxDrag(dragAmount, screenWidth, screenHeight)
                                }
                            },