1package core
2
3type borderKind int
4
5const (
6 borderColumn borderKind = iota + 1
7 borderRow
8)
9
10// border is an output-local hit zone between consecutive columns or rows.
11type border struct {
12 kind borderKind
13 col, row int
14 rect Rect
15}
16
17// borderMinWidth is the minimum logical pixel width for a border resize.
18// The effective minimum also includes 5% of the lane and four border widths.
19const borderMinWidth = 48
20
21func borderZone(a, b, pad int) (int, int) {
22 mid := (a + b) / 2
23 lo, hi := min(a, mid-pad), max(b, mid+pad)
24 return lo, max(hi-lo, 1)
25}
26
27func (w *Workspace) borderAt(x, y, pad int) (border, bool) {
28 if w.fullscreen != 0 || !contains(w.Usable, x, y) {
29 return border{}, false
30 }
31 // Overview and preview are Monitor presentation, not Workspace state. Their
32 // caller must not ask for workspace borders while presenting those scenes.
33 for _, p := range w.Layout() {
34 if p.Floating && !p.Hidden && !p.Below && contains(p.Rect, x, y) {
35 return border{}, false
36 }
37 }
38 rs := w.columnRects()
39 fixedFull := w.Overflow == OverflowFixed && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth
40 for i, r := range rs {
41 if fixedFull && i != w.Focus {
42 continue
43 }
44 if r.X+r.W <= w.Usable.X || r.X >= w.Usable.X+w.Usable.W || r.Y+r.H <= w.Usable.Y || r.Y >= w.Usable.Y+w.Usable.H {
45 continue
46 }
47 rows := rowRects(r, w.Columns[i], w.gap())
48 for j := 0; j+1 < len(rows); j++ {
49 lo, size := borderZone(rows[j].Y+rows[j].H, rows[j+1].Y, pad)
50 b := border{kind: borderRow, col: i, row: j, rect: Rect{X: r.X, Y: lo, W: r.W, H: size}}
51 if contains(b.rect, x, y) {
52 return b, true
53 }
54 }
55 if fixedFull || w.Overflow == OverflowFixed && !w.laneRow() || i+1 >= len(rs) || w.laneOf(i) != w.laneOf(i+1) || w.Columns[i].FullWidth || w.Columns[i+1].FullWidth {
56 continue
57 }
58 next := rs[i+1]
59 if next.X >= w.Usable.X+w.Usable.W || next.X+next.W <= w.Usable.X {
60 continue
61 }
62 top, bottom := max(r.Y, next.Y), min(r.Y+r.H, next.Y+next.H)
63 lo, size := borderZone(r.X+r.W, next.X, pad)
64 b := border{kind: borderColumn, col: i, rect: Rect{X: lo, Y: top, W: size, H: max(bottom-top, 0)}}
65 if contains(b.rect, x, y) {
66 return b, true
67 }
68 }
69 return border{}, false
70}
71
72// pixelShare rounds upward so a stamped width reproduces its integer pixels.
73func pixelShare(pixels, total int) int {
74 if total <= 0 {
75 return 0
76 }
77 return (pixels*10000 + total - 1) / total
78}
79
80// setBorder moves border b to pos and reports whether any span or share
81// changed (stamping a lane or a column's shares counts as a change).
82func (w *Workspace) setBorder(b border, pos int) bool {
83 if b.col < 0 || b.col >= len(w.Columns) || w.fullscreen != 0 {
84 return false
85 }
86 rs := w.columnRects()
87 r := rs[b.col]
88 if b.kind == borderRow {
89 c := &w.Columns[b.col]
90 if b.row < 0 || b.row+1 >= len(c.Windows) || len(c.Windows)*minShare > 10000 {
91 return false
92 }
93 rows := rowRects(r, *c, w.gap())
94 avail := max(r.H-(len(rows)-1)*w.gap(), 0)
95 if avail == 0 {
96 return false
97 }
98 stamped := !validShares(*c)
99 if stamped {
100 c.Shares = make([]int, len(rows))
101 used := 0
102 for i := range rows {
103 // Stamp the middle of the pixel bucket, leaving room for
104 // pair-preserving rounding on subsequent resizes.
105 c.Shares[i] = (rows[i].H*10000 + 5000) / avail
106 used += c.Shares[i]
107 }
108 c.Shares[len(rows)-1] += 10000 - used
109 }
110 pair := c.Shares[b.row] + c.Shares[b.row+1]
111 if pair < 2*minShare {
112 return stamped
113 }
114 target := pixelShare(pos-w.gap()/2-rows[b.row].Y, avail)
115 target = min(max(target, minShare), pair-minShare)
116 if b.row+1 < len(rows)-1 {
117 target = pairPixelShare(target, pair, avail, rows[b.row].H+rows[b.row+1].H, minShare)
118 }
119 changed := stamped || c.Shares[b.row] != target
120 c.Shares[b.row], c.Shares[b.row+1] = target, pair-target
121 return changed
122 }
123 if b.kind != borderColumn || b.col+1 >= len(w.Columns) || w.laneOf(b.col) != w.laneOf(b.col+1) || w.Overflow == OverflowFixed && !w.laneRow() || w.Columns[b.col].FullWidth || w.Columns[b.col+1].FullWidth {
124 return false
125 }
126 first, last := w.laneBounds(w.laneOf(b.col))
127 avail := max(w.Usable.W-2*w.gap(), 0)
128 if w.policy().equalCells {
129 avail = max(w.Usable.W-w.gap()*(last-first+2), 0)
130 }
131 if avail == 0 {
132 return false
133 }
134 stamped := !w.validSpans(first, last)
135 if stamped {
136 // Mutates spans, so it must not cache validity across this loop.
137 w.spanSeq++
138 used := 0
139 for i := first; i <= last; i++ {
140 w.Columns[i].Span = max((w.columnWidthFor(i, true)*10000+5000)/avail, 1)
141 w.Columns[i].spanStamp = w.spanSeq
142 used += w.Columns[i].Span
143 }
144 if w.policy().equalCells {
145 w.Columns[last].Span += 10000 - used
146 }
147 }
148 minimum := pixelShare(max(avail/20, 4*w.border, borderMinWidth), avail)
149 target := pixelShare(pos-w.gap()/2-r.X, avail)
150 if !w.policy().equalCells {
151 span := min(max(target, min(minimum, 10000)), 10000)
152 changed := stamped || w.Columns[b.col].Span != span
153 w.Columns[b.col].Span = span
154 return changed
155 }
156 pair := w.Columns[b.col].Span + w.Columns[b.col+1].Span
157 if pair < 2*minimum {
158 return stamped
159 }
160 target = min(max(target, minimum), pair-minimum)
161 if b.col+1 < last {
162 target = pairPixelShare(target, pair, avail, rs[b.col].W+rs[b.col+1].W, minimum)
163 }
164 changed := stamped || w.Columns[b.col].Span != target
165 w.Columns[b.col].Span, w.Columns[b.col+1].Span = target, pair-target
166 return changed
167}
168
169func (w *Workspace) resetBorder(b border) {
170 if b.col < 0 || b.col >= len(w.Columns) {
171 return
172 }
173 switch b.kind {
174 case borderColumn:
175 w.clearSpans(b.col)
176 case borderRow:
177 w.Columns[b.col].Shares = nil
178 }
179}
180
181// pairPixelShare keeps the pair's combined integer width unchanged as well as
182// its basis-point sum. Independent floors can otherwise shift later tiles by
183// one pixel even when the sum of the two shares is constant.
184func pairPixelShare(target, pair, avail, pixels, minimum int) int {
185 left := avail * target / 10000
186 right := pixels - left
187 lo := max(pixelShare(left, avail), pair-pixelShare(right+1, avail)+1, minimum)
188 hi := min(pixelShare(left+1, avail)-1, pair-pixelShare(right, avail), pair-minimum)
189 if lo <= hi {
190 return min(max(target, lo), hi)
191 }
192 return target
193}