1 /*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.settings.connecteddevice.display
18
19 import android.graphics.PointF
20 import android.graphics.RectF
21
22 import java.util.Locale
23
24 import kotlin.math.max
25 import kotlin.math.min
26
27 // These extension methods make calls to min and max chainable.
Floatnull28 fun Float.atMost(n: Number): Float = min(this, n.toFloat())
29 fun Float.atLeast(n: Number): Float = max(this, n.toFloat())
30
31 /**
32 * Contains the parameters needed for transforming global display coordinates to and from topology
33 * pane coordinates. This is necessary for implementing an interactive display topology pane. The
34 * pane allows dragging and dropping display blocks into place to define the topology. Conversion to
35 * pane coordinates is necessary when rendering the original topology. Conversion in the other
36 * direction, to display coordinates, is necessary for resolve a drag position to display space.
37 *
38 * The topology pane coordinates are physical pixels and represent the relative position from the
39 * upper-left corner of the pane. It uses a scale optimized for showing all displays with minimal
40 * or no scrolling. The display coordinates are floating point and the origin can be in any
41 * position. In practice the origin will be the upper-left coordinate of the primary display.
42 *
43 * @param paneWidth width of the pane in view coordinates
44 * @param minEdgeLength the smallest length permitted of a display block. This should be set based
45 * on accessibility requirements, but also accounting for padding that appears
46 * around each button.
47 * @param maxEdgeLength the longest width or height permitted of a display block. This will limit
48 * the amount of dragging and scrolling the user will need to do to set the
49 * arrangement.
50 * @param displaysPos the absolute topology coordinates for each display in the topology.
51 */
52 class TopologyScale(
53 paneWidth: Int, minEdgeLength: Float, maxEdgeLength: Float,
54 displaysPos: Collection<RectF>) {
55 /** Scale of block sizes to real-world display sizes. Should be less than 1. */
56 val blockRatio: Float
57
58 /** Height of topology pane needed to allow all display blocks to appear with some padding. */
59 val paneHeight: Float
60
61 /** Pane's X view coordinate that corresponds with topology's X=0 coordinate. */
62 val originPaneX: Float
63
64 /** Pane's Y view coordinate that corresponds with topology's Y=0 coordinate. */
65 val originPaneY: Float
66
67 init {
68 val displayBounds = RectF(
69 Float.MAX_VALUE, Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE)
70 var smallestDisplayDim = Float.MAX_VALUE
71 var biggestDisplayDim = Float.MIN_VALUE
72
73 // displayBounds is the smallest rect encompassing all displays, in display space.
74 // smallestDisplayDim is the size of the smallest display edge, in display space.
75 for (pos in displaysPos) {
76 displayBounds.union(pos)
77 smallestDisplayDim = minOf(smallestDisplayDim, pos.height(), pos.width())
78 biggestDisplayDim = maxOf(biggestDisplayDim, pos.height(), pos.width())
79 }
80
81 // Initialize blockRatio such that there is 20% padding on left and right sides of the
82 // display bounds.
83 blockRatio = (paneWidth * 0.6 / displayBounds.width()).toFloat()
84 // If the `ratio` is set too high because one of the displays will have an edge
85 // greater than maxEdgeLength(px) long, decrease it such that the largest edge is
86 // that long.
87 .atMost(maxEdgeLength / biggestDisplayDim)
88 // Also do the opposite of the above, this latter step taking precedence for a11y
89 // requirements.
90 .atLeast(minEdgeLength / smallestDisplayDim)
91
92 // A tall pane is likely to result in more scrolling. So we
93 // prevent the height from growing too large here, by limiting vertical padding to
94 // 1.5x of the minEdgeLength on each side. This keeps a comfortable amount of
95 // padding without it resulting in too much deadspace.
96 paneHeight = blockRatio * displayBounds.height() + minEdgeLength * 3f
97
98 // Set originPaneXY (the location of 0,0 in display space in the pane's coordinate system)
99 // such that the display bounds rect is centered in the pane.
100 // It is unlikely that either of these coordinates will be negative since blockRatio has
101 // been chosen to allow 20% padding around each side of the display blocks. However, the
102 // a11y requirement applied above (minEdgeLength / smallestDisplayDim) may cause the blocks
103 // to not fit. This should be rare in practice, and can be worked around by moving the
104 // settings UI to a larger display.
105 val blockMostLeft = (paneWidth - displayBounds.width() * blockRatio) / 2
106 val blockMostTop = (paneHeight - displayBounds.height() * blockRatio) / 2
107
108 originPaneX = blockMostLeft - displayBounds.left * blockRatio
109 originPaneY = blockMostTop - displayBounds.top * blockRatio
110 }
111
112 /** Transforms coordinates in view pane space to display space. */
113 fun paneToDisplayCoor(paneX: Float, paneY: Float): PointF {
114 return PointF((paneX - originPaneX) / blockRatio, (paneY - originPaneY) / blockRatio)
115 }
116
117 /** Transforms coordinates in display space to view pane space. */
118 fun displayToPaneCoor(displayX: Float, displayY: Float): PointF {
119 return PointF(displayX * blockRatio + originPaneX, displayY * blockRatio + originPaneY)
120 }
121
122 override fun toString() : String {
123 return String.format(
124 Locale.ROOT,
125 "{TopologyScale blockRatio=%f originPaneXY=%.1f,%.1f paneHeight=%.1f}",
126 blockRatio, originPaneX, originPaneY, paneHeight)
127 }
128 }
129