1package core
2
3import "github.com/bnema/neferwl/internal/ports"
4
5// Two modes. Without gaps (gap 0), borders are tmux-style lines (like tmux
6// screen-redraw.c): lines separate visible panes, never outline a lone tile
7// or a fullscreen/zoomed tile. Adjacent tiles share one line, owned (and
8// inset) by the left/top tile; focus lights only the adjacent sides of the
9// focused tile, including corners. With exactly two visible tiles, each
10// lights its own half of the shared line (left/top first). With gaps, each
11// tile owns a full four-sided border, like a float, and lights it whole,
12// even when alone. Hidden, scrolled-off, under-panel, other-workspace and
13// other-output tiles do not count, nor do tiles that share no line. Lines
14// at the edges of the usable area (output or panels) are omitted. Floating
15// windows also get their own full border, drawn with that window
16// (Separator.Window), even when it is the only visible window. Only the
17// focused output's focused window can light lines; keyboard grabs by popups
18// and layers do not change the underlying window's visual focus.
19
20var sides = [...]ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom}
21
22// separators are the lines of the windows seen in v, each clipped to its
23// window's area, inactive ones first so active ones draw over them. lit
24// says whether the focused tile shows focus (only on the focused output).
25func separators(ps []Placement, width, gap int, v viewport, lit bool) []ports.Separator {
26 if width <= 0 {
27 return nil
28 }
29 o := v.frame
30 var out []ports.Separator
31 add := func(p *Placement, r Rect, active bool) {
32 // A line cannot belong to a scrolled-off window, bleed onto
33 // another output or, for a tile, over a panel, even when a
34 // neighboring tile remains visible.
35 a := v.area(*p)
36 x0, y0 := max(r.X, a.X), max(r.Y, a.Y)
37 x1, y1 := min(r.X+r.W, a.X+a.W), min(r.Y+r.H, a.Y+a.H)
38 if x0 >= x1 || y0 >= y1 {
39 return
40 }
41 r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
42 var id WindowID
43 if p.Floating || gap > 0 {
44 id = p.ID
45 }
46 if out == nil {
47 // One line per side of every tile, and the focused one's four
48 // lit lines: the common case grows no further.
49 out = make([]ports.Separator, 0, 4*len(ps)+4)
50 }
51 out = append(out, ports.Separator{Rect: r, Active: active, Window: id})
52 }
53 var focused *Placement
54 tiles := 0
55 for i := range ps {
56 p := &ps[i]
57 // A leaving float keeps its own lines while it fades; a leaving
58 // tile's were shared with its neighbours, which re-flowed.
59 if p.Fullscreen || !(v.shows(*p) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) {
60 continue
61 }
62 if gap > 0 {
63 for _, s := range sides {
64 add(p, strip(p.Rect, s, 0, width), p.Focused && lit)
65 }
66 continue
67 }
68 // Tiles sharing a line: Neighbors only counts tiles visible in
69 // the usable area, not those under a panel.
70 if !p.Floating && p.Neighbors != 0 {
71 tiles++
72 }
73 for _, s := range sides {
74 if p.Inset&s != 0 {
75 add(p, strip(p.Rect, s, 0, width), false)
76 }
77 }
78 if p.Focused && lit {
79 focused = p
80 }
81 }
82 if focused == nil {
83 return out
84 }
85 f := focused
86 for _, s := range sides {
87 var line ports.Rect
88 switch {
89 case f.Neighbors&s != 0 && f.Inset&s != 0:
90 line = strip(f.Rect, s, 0, width)
91 case f.Neighbors&s != 0:
92 line = strip(f.Rect, s, width, width)
93 case f.Inset&s != 0:
94 // A float's own border.
95 add(f, strip(f.Rect, s, 0, width), true)
96 continue
97 default:
98 continue
99 }
100 line = closeCorners(line, *f, s, width)
101 // With gaps the loop above continued, so two tiles share one line.
102 if tiles == 2 {
103 line = half(line, s)
104 }
105 add(f, line, true)
106 }
107 return out
108}
109
110// strip is the b-wide strip along side s of r: inside r when out is 0,
111// else just outside it (the neighbor's line).
112func strip(r Rect, s ports.Sides, out, b int) Rect {
113 b = min(b, max(r.W/2, out), max(r.H/2, out))
114 switch s {
115 case ports.SideLeft:
116 return Rect{X: r.X - out, Y: r.Y, W: b, H: r.H}
117 case ports.SideRight:
118 return Rect{X: r.X + r.W - b + out, Y: r.Y, W: b, H: r.H}
119 case ports.SideTop:
120 return Rect{X: r.X, Y: r.Y - out, W: r.W, H: b}
121 }
122 return Rect{X: r.X, Y: r.Y + r.H - b + out, W: r.W, H: b}
123}
124
125// closeCorners runs a line along side s of p b further at each end where
126// the crossing line lies outside p (a neighbor's), so lit lines meet.
127func closeCorners(line Rect, p Placement, s ports.Sides, b int) Rect {
128 outside := func(s ports.Sides) bool { return p.Neighbors&s != 0 && p.Inset&s == 0 }
129 if s == ports.SideLeft || s == ports.SideRight {
130 if outside(ports.SideTop) {
131 line.Y, line.H = line.Y-b, line.H+b
132 }
133 if outside(ports.SideBottom) {
134 line.H += b
135 }
136 return line
137 }
138 if outside(ports.SideLeft) {
139 line.X, line.W = line.X-b, line.W+b
140 }
141 if outside(ports.SideRight) {
142 line.W += b
143 }
144 return line
145}
146
147// half keeps the half of a line owned by the tile on side s of it: the
148// first half for the left or top tile, else the second.
149func half(r Rect, s ports.Sides) Rect {
150 switch s {
151 case ports.SideRight:
152 r.H /= 2
153 case ports.SideLeft:
154 r.Y, r.H = r.Y+r.H/2, r.H-r.H/2
155 case ports.SideBottom:
156 r.W /= 2
157 case ports.SideTop:
158 r.X, r.W = r.X+r.W/2, r.W-r.W/2
159 }
160 return r
161}
162
163// windowRadius is the corner radius of p: floats always, tiles only with
164// gaps, never fullscreen windows.
165func windowRadius(p Placement, gap, radius int) int {
166 if !p.Fullscreen && (p.Floating || gap > 0) {
167 return radius
168 }
169 return 0
170}