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

Commit 00279cf0 authored by Nikki Moteva's avatar Nikki Moteva
Browse files

System UI: Make opposite corners static when dragging on a corner

Each corners of the box is treated separately, and when a corner moves,
we will now manually move the edges of the rectangle.

Screencast: http://shortn/_buldmWsxdu

Bug: 417533606
Test: Manual
Flag: com.android.systemui.desktop_screen_capture
Change-Id: Ica4d3b0eabb89831728f4cd2999872b987b1fd8d
parent 3444fa0a
Loading
Loading
Loading
Loading
+128 −132
Original line number Diff line number Diff line
@@ -33,82 +33,112 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.min

/** An enum to identify each of the four corners of the rectangle. */
enum class Corner {
    TopLeft,
    TopRight,
    BottomLeft,
    BottomRight,
/**
 * 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) {
    TopLeft(Alignment.TopStart),
    TopRight(Alignment.TopEnd),
    BottomLeft(Alignment.BottomStart),
    BottomRight(Alignment.BottomEnd),
}

/**
 * A stateful composable that manages the size of a resizable RegionBox.
 * A stateful composable that manages the size and position of a resizable RegionBox.
 *
 * @param initialWidth The initial width of the box.
 * @param initialHeight The initial height of the box.
 * @param onDragEnd A callback function that is invoked with the final width and height when the
 *   user finishes a drag gesture.
 * @param onDragEnd A callback function that is invoked with the final offset, width, and height
 *   when the user finishes a drag gesture.
 * @param initialOffset The initial top-left offset of the box. Default is (0, 0), which is the
 *   parent's top-left corner.
 * @param modifier The modifier to be applied to the composable.
 */
@Composable
fun RegionBox(
    initialWidth: Dp,
    initialHeight: Dp,
    onDragEnd: (width: Dp, height: Dp) -> Unit,
    onDragEnd: (offset: Offset, width: Dp, height: Dp) -> Unit,
    initialOffset: Offset = Offset.Zero,
    modifier: Modifier = Modifier,
) {
    // The minimum size allowed for the rectangle.
    // TODO(b/422565042): change when its value is finalized.
    val MIN_SIZE = 48.dp
    val minSize = 48.dp

    // The current state of the box.
    var width by remember { mutableStateOf(initialWidth) }
    var height by remember { mutableStateOf(initialHeight) }
    val density = LocalDensity.current
    val minSizePx = remember(density) { with(density) { minSize.toPx() } }

    val onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Dp, maxHeight: Dp) -> Unit =
        { dragAmount, corner, maxWidth, maxHeight ->
            val (dragX, dragY) = dragAmount
    // State for the region box's geometry.
    var rect by remember {
        mutableStateOf(
            with(density) {
                // Calculate the potential new width and height based on the drag amount.
                // The width and height are increased or decreased by twice the drag amount as the
                // drag is calculated from the center of the box, so we need to add twice the drag
                // amount to account for both sides of the box.
                val newWidth =
                    when (corner) {
                        Corner.TopLeft,
                        Corner.BottomLeft -> width - (dragX.toDp() * 2)
                        Corner.TopRight,
                        Corner.BottomRight -> width + (dragX.toDp() * 2)
                // offset is how far from the parent's top-left corner the box should be placed.
                Rect(offset = initialOffset, size = Size(initialWidth.toPx(), initialHeight.toPx()))
            }
                val newHeight =
                    when (corner) {
                        Corner.TopLeft,
                        Corner.TopRight -> height - (dragY.toDp() * 2)
                        Corner.BottomLeft,
                        Corner.BottomRight -> height + (dragY.toDp() * 2)
        )
    }

                // The new width and height cannot be smaller than the minimum allowed, or bigger
                // than the screen size.
                width = max(min(newWidth, maxWidth), MIN_SIZE)
                height = max(min(newHeight, maxHeight), MIN_SIZE)
    val onDrag: (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.
            var newLeft = rect.left
            var newTop = rect.top
            var newRight = rect.right
            var newBottom = rect.bottom

            val (dragX, dragY) = dragAmount

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

                newLeft = potentialNewLeft.coerceIn(0f, rightLimitForMinWidth)
            } else {
                val potentialNewRight = rect.right + dragX
                val leftLimitForMinWidth = rect.left + minSizePx

                newRight = potentialNewRight.coerceIn(leftLimitForMinWidth, maxWidth)
            }

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

                newTop = potentialNewTop.coerceIn(0f, bottomLimitForMinHeight)
            } else {
                val potentialNewBottom = rect.bottom + dragY
                val topLimitForMinHeight = rect.top + minSizePx

                newBottom = potentialNewBottom.coerceIn(topLimitForMinHeight, maxHeight)
            }

            rect = Rect(newLeft, newTop, newRight, newBottom)
        }

    ResizableRectangle(
        width = width,
        height = height,
        rect = rect,
        onDrag = onDrag,
        onDragEnd = { onDragEnd(width, height) },
        onDragEnd = {
            onDragEnd(
                Offset(rect.left, rect.top),
                with(density) { rect.width.toDp() },
                with(density) { rect.height.toDp() },
            )
        },
        modifier = modifier,
    )
}
@@ -116,28 +146,27 @@ fun RegionBox(
/**
 * A box with border lines and centered corner knobs that can be resized.
 *
 * @param width The current width of the box.
 * @param height The current height of the box.
 * @param rect The current geometry of the region box.
 * @param onDrag Callback invoked when a knob is dragged.
 * @param onDragEnd Callback invoked when a drag gesture finishes.
 * @param modifier The modifier to be applied to the composable.
 */
@Composable
private fun ResizableRectangle(
    width: Dp,
    height: Dp,
    onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Dp, maxHeight: Dp) -> Unit,
    rect: Rect,
    onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Float, maxHeight: Float) -> Unit,
    onDragEnd: () -> Unit,
    modifier: Modifier = Modifier,
) {
    // The diameter of the resizable knob on each corner of the region box.
    val KNOB_DIAMETER = 8.dp
    val knobDiameter = 8.dp
    // The width of the border stroke around the region box.
    val BORDER_STROKE_WIDTH = 4.dp
    val borderStrokeWidth = 4.dp

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

    val density = LocalDensity.current

    // The box that contains the whole screen.
@@ -148,91 +177,57 @@ private fun ResizableRectangle(
                // .onSizeChanged gives us the final size of this box, which is the screen size,
                // after it has been drawn.
                .onSizeChanged { sizeInPixels ->
                    screenWidth = with(density) { sizeInPixels.width.toDp() }
                    screenHeight = with(density) { sizeInPixels.height.toDp() }
                },
        contentAlignment = Alignment.Center,
                    screenWidth = sizeInPixels.width.toFloat()
                    screenHeight = sizeInPixels.height.toFloat()
                }
    ) {
        // The box container for the region box and its knobs.
        Box(modifier = Modifier.size(width, height)) {
        Box(
            modifier =
                Modifier.graphicsLayer(translationX = rect.left, translationY = rect.top)
                    .size(
                        width = with(density) { rect.width.toDp() },
                        height = with(density) { rect.height.toDp() },
                    )
        ) {
            // The main box for the region selection.
            Box(
                modifier =
                    Modifier.fillMaxSize()
                        .border(BORDER_STROKE_WIDTH, MaterialTheme.colorScheme.onSurfaceVariant)
                        .border(borderStrokeWidth, MaterialTheme.colorScheme.onSurfaceVariant)
            )

            // The offset is half of the knob diameter so that it is centered.
            val knobOffset = KNOB_DIAMETER / 2
            val knobOffset = knobDiameter / 2

            // Top left knob
            Knob(
                diameter = KNOB_DIAMETER,
                modifier =
                    Modifier.align(Alignment.TopStart)
                        .offset(x = -knobOffset, y = -knobOffset)
                        .pointerInput(Unit) {
                            detectDragGestures(
                                onDragEnd = onDragEnd,
                                onDrag = { change, dragAmount ->
                                    change.consume()
                                    onDrag(dragAmount, Corner.TopLeft, screenWidth, screenHeight)
                                },
                            )
                        },
            )
            // Create knobs by looping through the Corner enum values
            Corner.entries.forEach { corner ->
                val xOffset: Dp
                val yOffset: Dp

            // Top right knob
            Knob(
                diameter = KNOB_DIAMETER,
                modifier =
                    Modifier.align(Alignment.TopEnd)
                        .offset(x = knobOffset, y = -knobOffset)
                        .pointerInput(Unit) {
                            detectDragGestures(
                                onDragEnd = onDragEnd,
                                onDrag = { change, dragAmount ->
                                    change.consume()
                                    onDrag(dragAmount, Corner.TopRight, screenWidth, screenHeight)
                                },
                            )
                        },
            )
                if (corner == Corner.TopLeft || corner == Corner.BottomLeft) {
                    xOffset = -knobOffset
                } else {
                    xOffset = knobOffset
                }

            // Bottom left knob
            Knob(
                diameter = KNOB_DIAMETER,
                modifier =
                    Modifier.align(Alignment.BottomStart)
                        .offset(x = -knobOffset, y = knobOffset)
                        .pointerInput(Unit) {
                            detectDragGestures(
                                onDragEnd = onDragEnd,
                                onDrag = { change, dragAmount ->
                                    change.consume()
                                    onDrag(dragAmount, Corner.BottomLeft, screenWidth, screenHeight)
                                },
                            )
                        },
            )
                if (corner == Corner.TopLeft || corner == Corner.TopRight) {
                    yOffset = -knobOffset
                } else {
                    yOffset = knobOffset
                }

            // Bottom right knob
                Knob(
                diameter = KNOB_DIAMETER,
                    diameter = knobDiameter,
                    modifier =
                    Modifier.align(Alignment.BottomEnd)
                        .offset(x = knobOffset, y = knobOffset)
                        .pointerInput(Unit) {
                        Modifier.align(corner.alignment)
                            .offset(x = xOffset, y = yOffset)
                            .pointerInput(corner, screenWidth, screenHeight) {
                                detectDragGestures(
                                    onDragEnd = onDragEnd,
                                    onDrag = { change, dragAmount ->
                                        change.consume()
                                    onDrag(
                                        dragAmount,
                                        Corner.BottomRight,
                                        screenWidth,
                                        screenHeight,
                                    )
                                        onDrag(dragAmount, corner, screenWidth, screenHeight)
                                    },
                                )
                            },
@@ -240,6 +235,7 @@ private fun ResizableRectangle(
            }
        }
    }
}

/**
 * The circular knob on each corner of the box used for dragging each corner.