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