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