-
Notifications
You must be signed in to change notification settings - Fork 1
/
heads.go
295 lines (259 loc) · 8.94 KB
/
heads.go
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
// Copyright (c) 2018, Mark "Happy-Ferret" Bauermeister
//
// This software may be modified and distributed under the terms
// of the BSD license. See the LICENSE file for details.
package heads
import (
"fmt"
"github.com/BurntSushi/xgbutil"
"github.com/BurntSushi/xgbutil/ewmh"
"github.com/BurntSushi/xgbutil/xinerama"
"github.com/BurntSushi/xgbutil/xrect"
"github.com/BurntSushi/xgbutil/xwindow"
"github.com/Anima-OS/Wonderland/logger"
"github.com/Anima-OS/Wonderland/workspace"
)
type Heads struct {
X *xgbutil.XUtil
workarea xinerama.Heads // Slice of heads with struts applied.
geom xinerama.Heads // Raw geometry of heads.
active int // Index in workarea/geom/visibles of active head.
Workspaces *workspace.Workspaces // Slice of all available workspaces.
visibles []*workspace.Workspace // Slice of all visible workspaces.
}
func NewHeads(X *xgbutil.XUtil, defaultLayout string) *Heads {
hds := &Heads{
X: X,
active: 0,
}
hds.Workspaces = workspace.NewWorkspaces(X, hds, defaultLayout)
return hds
}
func (hds *Heads) Initialize(clients Clients) {
hds.geom = query(hds.X)
// Check if the number of workspaces is less than the number of heads.
if len(hds.Workspaces.Wrks) < len(hds.geom) {
logger.Error.Fatalf(
"There must be at least %d workspaces (one for each head).",
len(hds.geom))
}
// To make things simple, set the first workspace to be the
// active workspace, and setup the visibles slice as the first N workspaces
// where N is the number of heads.
// TODO: There may be a saner way of orienting workspaces when the
// phyiscal heads change. Implement it!
hds.ActivateWorkspace(hds.Workspaces.Wrks[0])
hds.visibles = make([]*workspace.Workspace, len(hds.geom))
for i := 0; i < len(hds.geom); i++ {
hds.visibles[i] = hds.Workspaces.Wrks[i]
}
// Apply the struts set by clients to the workarea geometries.
// This will fill in the hds.workarea slice.
hds.ApplyStruts(clients)
// Now show only the visibles and hide everything else.
for _, wrk := range hds.Workspaces.Wrks {
if wrk.IsVisible() {
wrk.Show()
} else {
wrk.Hide()
}
}
}
func (hds *Heads) Reload(clients Clients) {
newGeom := query(hds.X)
// Check if the number of workspaces is less than the number of heads.
if len(hds.Workspaces.Wrks) < len(newGeom) {
logger.Error.Fatalf(
"There must be at least %d workspaces (one for each head).",
len(newGeom))
}
logger.Message.Printf("Root window geometry had changed. Mirgrating "+
"from %d heads to %d heads.", len(hds.visibles), len(newGeom))
// Here comes the tricky part. We may have more, less or the same number
// of heads. But we'd like there to be as much of an overlap as possible
// between the heads that were visible before and the heads that will
// be visible. If we have the same number of heads as before, then we
// don't much care about this.
if len(hds.visibles) < len(newGeom) {
// We have more heads than we had before. So let's just expand our
// visibles with some new workspaces. Remember, we're guaranteed to
// have at least as many workspaces as heads.
// We also leave the currently active workspace alone.
for i := len(hds.visibles); i < len(newGeom); i++ {
// Find an available (i.e., hidden) workspace.
for _, wrk := range hds.Workspaces.Wrks {
if hds.VisibleIndex(wrk) == -1 {
hds.visibles = append(hds.visibles, wrk)
break
}
}
}
hds.geom = newGeom
} else if len(hds.visibles) > len(newGeom) {
// We now have fewer heads than we had before, so we'll reconstruct
// our list of visibles, with care to keep the same ordering and to
// keep the currently workspace still visible. (I believe this behavior
// to be the least surprising to the user.)
oldActive := hds.visibles[hds.active]
oldvis := hds.visibles
newvis := make([]*workspace.Workspace, len(newGeom))
newActive := -1
newi := 0
for oldi := 0; oldi < len(oldvis) && newi < len(newvis); oldi++ {
// We always add this workspace, UNLESS we have only one spot left
// and haven't added the active workspace yet. (We reserve that
// last spot for the active workspace.)
wrk := oldvis[oldi]
if newActive == -1 && newi == len(newvis)-1 && !wrk.IsActive() {
continue
}
newvis[newi] = wrk
if wrk.IsActive() {
newActive = newi
}
newi++
}
// Now that we've collected our new visibles list, we need to hide
// all of the workspaces. (We'll show them later.) This is so that
// they get properly refreshed into the right locations on the screen.
for _, wrk := range hds.Workspaces.Wrks {
wrk.Hide()
}
hds.visibles = newvis
hds.geom = newGeom
hds.ActivateWorkspace(hds.visibles[newActive])
if oldActive != hds.visibles[hds.active] {
panic(fmt.Sprintf("BUG: Old active workspace %s is not the same "+
"as the new active workspace.",
oldActive, hds.visibles[hds.active]))
}
}
// Protect my sanity...
if len(hds.visibles) != len(hds.geom) {
panic(fmt.Sprintf("BUG: length of visibles (%d) != length of "+
"geometry (%d)", len(hds.visibles), len(hds.geom)))
}
// Apply the struts set by clients to the workarea geometries.
// This will fill in the hds.workarea slice.
hds.ApplyStruts(clients)
// Now show only the visibles and hide everything else.
for _, wrk := range hds.Workspaces.Wrks {
if wrk.IsVisible() {
wrk.Show()
} else {
wrk.Hide()
}
}
}
func (hds *Heads) ApplyStruts(clients Clients) {
hds.workarea = make(xinerama.Heads, len(hds.geom))
for i, hd := range query(hds.X) {
hds.workarea[i] = xrect.New(hd.X(), hd.Y(), hd.Width(), hd.Height())
}
rgeom := xwindow.RootGeometry(hds.X)
for i := 0; i < clients.Len(); i++ {
c := clients.Get(i)
strut, _ := ewmh.WmStrutPartialGet(hds.X, c.Id())
if strut == nil {
continue
}
xrect.ApplyStrut(hds.workarea,
uint(rgeom.Width()), uint(rgeom.Height()),
strut.Left, strut.Right, strut.Top, strut.Bottom,
strut.LeftStartY, strut.LeftEndY,
strut.RightStartY, strut.RightEndY,
strut.TopStartX, strut.TopEndX,
strut.BottomStartX, strut.BottomEndX)
}
for _, wrk := range hds.Workspaces.Wrks {
wrk.Place()
}
for i := 0; i < clients.Len(); i++ {
c := clients.Get(i)
if c.IsMaximized() {
c.Remaximize()
}
}
hds.EwmhWorkarea()
}
// EwmhWorkarea sets the _NET_WORKAREA property. Generally, this property
// doesn't make much sense since multiple workspaces can be viewable at
// one time, and each workspace might have different workareas.
//
// However, if the EWMH is read loosely, we can update _NET_WORKAREA not just
// when the struts change, but also when the configuration of visible workspaces
// changes. Namely, only the visible workspaces have a valid geometry set
// in _NET_WORKAREA, while the rest are zeroed out.
//
// N.B. Fuck that. This interpretation of _NET_WORKAREA makes KDE go
// absolutely bonkers. I'm not sure if there is another sensible interpretation,
// so just don't set it.
func (hds *Heads) EwmhWorkarea() {
// areas := make([]ewmh.Workarea, len(hds.Workspaces.Wrks))
// for i, wrk := range hds.Workspaces.Wrks {
// if wrk.IsVisible() {
// geom := wrk.Geom()
// areas[i] = ewmh.Workarea{
// X: geom.X(),
// Y: geom.Y(),
// Width: geom.Width(),
// Height: geom.Height(),
// }
// } else {
// areas[i] = ewmh.Workarea{
// X: 0,
// Y: 0,
// Width: 0,
// Height: 0,
// }
// }
// }
// ewmh.WorkareaSet(hds.X, areas)
}
// Convert takes a source and a destination rect, along with a rect
// in the source's rectangle, and returns a new rect translated into the
// destination rect.
func Convert(rect, src, dest xrect.Rect) xrect.Rect {
nx, ny, nw, nh := xrect.Pieces(rect)
rectRatio := func(r xrect.Rect) float64 {
return float64(r.Width()) / float64(r.Height())
}
ratio := rectRatio(dest) / rectRatio(src)
nx = int(ratio*float64(nx-src.X())) + dest.X()
ny = int(ratio*float64(ny-src.Y())) + dest.Y()
// XXX: Allow window scaling as a config option.
return xrect.New(nx, ny, nw, nh)
}
// NumHeads returns the current number of heads that Wingo is using.
func (hds *Heads) NumHeads() int {
return len(hds.geom)
}
// NumConnected pings the Xinerama extension for a fresh tally of the number
// of heads currently active.
func (hds *Heads) NumConnected() int {
return len(query(hds.X))
}
func query(X *xgbutil.XUtil) xinerama.Heads {
if X.ExtInitialized("XINERAMA") {
heads, err := xinerama.PhysicalHeads(X)
if err != nil || len(heads) == 0 {
if err == nil {
logger.Warning.Printf("Could not find any physical heads " +
"with the Xinerama extension.")
} else {
logger.Warning.Printf("Could not load physical heads via "+
"Xinerama: %s", err)
}
logger.Warning.Printf("Assuming one head with size equivalent " +
"to the root window.")
} else {
return heads
}
}
// If we're here, then something went wrong or the Xinerama extension
// isn't available. So query the root window for its geometry and use that.
rgeom := xwindow.RootGeometry(X)
return xinerama.Heads{
xrect.New(rgeom.X(), rgeom.Y(), rgeom.Width(), rgeom.Height()),
}
}