aboutsummaryrefslogtreecommitdiffstats
path: root/config/quickshell/qs-hyprview-unified/layouts/SpiralLayout.qml
blob: bcfc7abb9d1f9adc43c879fe6dc0faec5633b3c1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
pragma Singleton
import Quickshell
import Quickshell.Hyprland

Singleton {
    id: root

    function doLayout(windowList, outerWidth, outerHeight, maxSplits) {
        var N = windowList.length
        if (N === 0) return []

        if (maxSplits === undefined) maxSplits = 3

        // Standard Gap: 0.8% of screen
        var rawGap = outerWidth * 0.008
        var gap = Math.max(8, Math.min(24, rawGap))

        // Primary Gap: The space between the first Big Window and the rest.
        // We make it 3x larger than the standard gap for emphasis.
        var primaryGap = gap * 3

        // Safe Area (90%)
        var contentScale = 0.90
        var useW = outerWidth * contentScale
        var useH = outerHeight * contentScale
        var offX = (outerWidth - useW) / 2
        var offY = (outerHeight - useH) / 2

        // Move Active Window to start
        var activeAddr = Hyprland.activeToplevel?.lastIpcObject?.address
        if (activeAddr) {
            var activeIdx = windowList.findIndex(it => it.lastIpcObject.address === activeAddr)
            if (activeIdx !== -1) {
                windowList = [windowList[activeIdx], ...windowList.filter(it => it !== windowList[activeIdx])]
            }
        }

        var result = []

        // Working area cursor
        var curX = offX
        var curY = offY
        var curW = useW
        var curH = useH

        // Items to process in Spiral mode
        var spiralCount = Math.min(N - 1, maxSplits)

        // Spiral cuts
        for (var k = 0; k < spiralCount; k++) {
            var sItem = windowList[k]
            var sBoxW, sBoxH
            var sBoxX = curX
            var sBoxY = curY

            // Logic change: Use 'primaryGap' only for the very first cut (k=0),
            // otherwise use standard 'gap'.
            var currentGap = (k === 0) ? primaryGap : gap

            if (curW > curH) { // Split Vertical
                // Calculate width subtracting the specific gap for this iteration
                sBoxW = (curW - currentGap) / 2
                sBoxH = curH

                // Shift working area for next items by the specific gap
                curX += sBoxW + currentGap
                curW -= (sBoxW + currentGap)
            } else { // Split Horizontal
                sBoxW = curW
                sBoxH = (curH - currentGap) / 2

                // Shift working area for next items by the specific gap
                curY += sBoxH + currentGap
                curH -= (sBoxH + currentGap)
            }

            // Aspect Fit
            var sw0 = (sItem.width > 0) ? sItem.width : 100
            var sh0 = (sItem.height > 0) ? sItem.height : 100
            var sScale = Math.min(sBoxW / sw0, sBoxH / sh0)

            result.push({
                win: sItem.win,
                x: sBoxX + (sBoxW - (sw0 * sScale))/2,
                y: sBoxY + (sBoxH - (sh0 * sScale))/2,
                width: sw0 * sScale,
                height: sh0 * sScale,
                isSpiral: true,
                index: k
            })
        }

        // Overflow grid
        var remainingItems = windowList.slice(spiralCount)
        var remN = remainingItems.length

        if (remN > 0) {
            // Standard Grid logic for the remaining box
            var bestCols = 1
            var bestScale = 0
            var TARGET_ASPECT = 16.0/9.0

            for (var c = 1; c <= remN; c++) {
                var r = Math.ceil(remN / c)
                var avW = curW - gap * (c - 1)
                var avH = curH - gap * (r - 1)
                if (avW <= 0 || avH <= 0) continue

                var cW = avW / c
                var cH = avH / r
                var sc = Math.min(cW / TARGET_ASPECT, cH)

                if (sc > bestScale) {
                    bestScale = sc
                    bestCols = c
                }
            }

            var remRows = Math.ceil(remN / bestCols)
            var finalCellW = (curW - gap * (bestCols - 1)) / bestCols
            var finalCellH = (curH - gap * (remRows - 1)) / remRows

            var gridContentH = remRows * finalCellH + (remRows - 1) * gap
            var gridStartY = curY + (curH - gridContentH) / 2

            for (var j = 0; j < remN; j++) {
                var rItem = remainingItems[j]
                var row = Math.floor(j / bestCols)
                var col = j % bestCols

                var itemsInRow = Math.min((row + 1) * bestCols, remN) - (row * bestCols)
                var rowW = itemsInRow * finalCellW + (itemsInRow - 1) * gap
                var rowStartX = curX + (curW - rowW) / 2

                var cellX = rowStartX + col * (finalCellW + gap)
                var cellY = gridStartY + row * (finalCellH + gap)

                var rw0 = (rItem.width > 0) ? rItem.width : 100
                var rh0 = (rItem.height > 0) ? rItem.height : 100
                var rSc = Math.min(finalCellW / rw0, finalCellH / rh0)

                result.push({
                    win: rItem.win,
                    x: cellX + (finalCellW - (rw0 * rSc))/2,
                    y: cellY + (finalCellH - (rh0 * rSc))/2,
                    width: rw0 * rSc,
                    height: rh0 * rSc,
                    isSpiral: false
                })
            }
        }

        return result
    }
}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage