chromium/ui/android/java/src/org/chromium/ui/util/WindowInsetsUtils.java

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.ui.util;

import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RegionIterator;
import android.util.Size;
import android.view.WindowInsets;

import androidx.annotation.NonNull;
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat.Type.InsetsType;

import org.chromium.base.Callback;
import org.chromium.base.Log;

import java.util.List;

/** Helper functions for working with WindowInsets and Rects. */
public final class WindowInsetsUtils {
    private static final String TAG = "WindowInsetsUtils";

    private static final Size DEFAULT_INSETS_FRAME = new Size(0, 0);
    private static final List<Rect> DEFAULT_INSETS_BOUNDING_RECTS = List.of();

    private static boolean sGetFrameMethodNotFound;
    private static boolean sGetBoundingRectsMethodNotFound;

    /** Private constructor to stop instantiation. */
    private WindowInsetsUtils() {}

    /**
     * Return the rect represented by the input {@link Insets}. Return an empty Rect if the input
     * |insets| inset more than one edge from the |windowRect| (i.e. has more than one non-zero
     * value over the four sides: left / top / right / bottom).
     *
     * <p><b>Example 1: </b><br>
     * windowRect = Rect(0, 0, 100, 50) // left: 0, top: 0, right: 100, bottom: 50 <br>
     * insets = (0, 20, 0, 0) // Inset 20 from the top. <br>
     * Output: Rect(0, 0, 100, 20) // Insets represent 20 from the top of the windowRect.
     *
     * <p><b>Example 2:</b> <br>
     * windowRect = Rect(0, 0, 100, 50) // left: 0, top: 0, right: 100, bottom: 50 <br>
     * insets = (0, 0, 30, 0) // Inset 30 from the right. <br>
     * Output: Rect(30, 0, 100, 50) // Insets represent 30 from the right of the windowRect,
     * starting from left=70
     *
     * <p><b>Example 3:</b> <br>
     * windowRect = Rect(0, 0, 100, 50) // left: 0, top: 0, right: 100, bottom: 50 <br>
     * insets = (0, 0, 10, 10) // Inset 10 from the right and 10 from bottom <br>
     * Output: Rect(0, 0, 0, 0) // Insets represent more than one edge and it's not an Rect.
     *
     * <p><b>Example 4:</b> <br>
     * windowRect = Rect(0, 0, 100, 50) // left: 0, top: 0, right: 100, bottom: 50 <br>
     * insets = (0, 0, 0, 0) // No insets from thw windowRect <br>
     * Output: Rect(0, 0, 0, 0) // Insets does not represent any rect.
     *
     * @param windowRect The rect representing the root view of the window.
     * @param insets Insets describing a certain type of a WindowInsts.
     * @return Rect that the insets represent in the windowRect. Empty rect if insets represent more
     *     than one edge.
     */
    public static @NonNull Rect toRectInWindow(@NonNull Rect windowRect, @NonNull Insets insets) {
        int sides = 0;
        Rect res = new Rect(windowRect);

        if (insets.left != 0) {
            res.right = windowRect.left + insets.left;
            sides++;
        }

        if (insets.top != 0) {
            if (sides > 0) return new Rect();
            res.bottom = windowRect.top + insets.top;
            sides++;
        }

        if (insets.right != 0) {
            if (sides > 0) return new Rect();
            res.left = windowRect.right - insets.right;
            sides++;
        }

        if (insets.bottom != 0) {
            if (sides > 0) return new Rect();
            res.top = windowRect.bottom - insets.bottom;
            sides++;
        }

        return sides == 1 ? res : new Rect();
    }

    /**
     * Get the Rect with the maximum width within the |regionRect| that is not blocked by any rects
     * within the |blockedRects|. This algorithm only prioritizes the width of the returned Rects,
     * so the returned area does not necessarily have the maximum area. If there are multiple rects
     * with the same width, this method will bias the first Rect found in the region.
     *
     * @see Region
     * @see RegionIterator
     * @param regionRect The un-blocked rect area.
     * @param blockedRects Areas within the regionRect that are blocked.
     * @return The widest Rect seen in the regionRect that's not blocked by any blockedRects.
     */
    public static @NonNull Rect getWidestUnoccludedRect(
            @NonNull Rect regionRect, List<Rect> blockedRects) {
        if (regionRect.isEmpty()) return regionRect;

        Region region = new Region(regionRect);
        for (Rect rect : blockedRects) {
            region.op(rect, Region.Op.DIFFERENCE);
        }
        Rect widestUnoccludedRect = new Rect();
        forEachRect(
                region,
                (rect) -> {
                    if (widestUnoccludedRect.width() < rect.width()) {
                        widestUnoccludedRect.set(rect);
                    }
                });
        return widestUnoccludedRect;
    }

    /** See {@link WindowInsets#getFrame()} for details. */
    @SuppressWarnings("NewApi")
    public static Size getFrameFromInsets(WindowInsets windowInsets) {
        // This invocation is wrapped in a try-catch block to allow backporting of the #getFrame()
        // API on pre-V devices. On pre-V devices not supporting this API, a default value will be
        // cached on the first failure and returned subsequently.
        if (sGetFrameMethodNotFound) return DEFAULT_INSETS_FRAME;
        try {
            return windowInsets == null ? DEFAULT_INSETS_FRAME : windowInsets.getFrame();
        } catch (NoSuchMethodError e) {
            Log.w(TAG, e.toString());
            sGetFrameMethodNotFound = true;
            return DEFAULT_INSETS_FRAME;
        }
    }

    /** See {@link WindowInsets#getBoundingRects(int)} for details. */
    @SuppressWarnings("NewApi")
    public static List<Rect> getBoundingRectsFromInsets(
            WindowInsets windowInsets, @InsetsType int insetType) {
        // This invocation is wrapped in a try-catch block to allow backporting of the
        // #getBoundingRects() API on pre-V devices. On pre-V devices not supporting this API, a
        // default value will be cached on the first failure and returned subsequently.
        if (sGetBoundingRectsMethodNotFound) return DEFAULT_INSETS_BOUNDING_RECTS;
        try {
            return windowInsets == null
                    ? DEFAULT_INSETS_BOUNDING_RECTS
                    : windowInsets.getBoundingRects(insetType);
        } catch (NoSuchMethodError e) {
            Log.w(TAG, e.toString());
            sGetBoundingRectsMethodNotFound = true;
            return DEFAULT_INSETS_BOUNDING_RECTS;
        }
    }

    private static void forEachRect(Region region, Callback<Rect> rectConsumer) {
        final RegionIterator it = new RegionIterator(region);
        final Rect rect = new Rect();
        while (it.next(rect)) {
            rectConsumer.onResult(rect);
        }
    }
}