1package core
2
3import (
4 "slices"
5
6 "github.com/bnema/neferwl/internal/ports"
7)
8
9// screen is one connected output: its monitor tree, scale, layer surfaces and
10// place in the global layout (ADR 011). Outputs are placed left to right, in
11// config order (output.<name>.*), then in connection order.
12type screen struct {
13 info ports.OutputInfo
14 mon *Monitor
15 scale, cfgScale float64
16 primary bool
17 x, y int // logical origin in the global layout
18 off bool // turned off by a client (output power management)
19 layers []ports.LayerSurface
20 placed []ports.SceneLayer
21 // capture holds the attached layers of the capture exclusion: shown over
22 // a fullscreen window (capture.go).
23 capture map[WindowID]bool
24 // capScene and capMarks are what the last scene carried (Capture and
25 // CaptureIndicators): immutable, shared by the next one while unchanged.
26 capScene *ports.SceneCapture
27 capMarks []ports.CaptureIndicator
28}
29
30func (s *screen) name() string { return s.info.Name }
31
32func (s *screen) placement() ports.OutputPlacement {
33 o := s.mon.Output()
34 return ports.OutputPlacement{Info: s.info, X: s.x, Y: s.y, Width: o.W, Height: o.H, Scale: s.scale, Primary: s.primary}
35}
36
37// setScale resizes the logical output; layer placement follows.
38func (s *screen) setScale(v float64) {
39 s.scale = SnapScale(v)
40 s.mon.SetOutput(logical(s.info.Width, s.scale), logical(s.info.Height, s.scale))
41 s.arrange()
42}
43
44// arrange places the layer surfaces and gives workspaces the usable area.
45func (s *screen) arrange() {
46 var usable ports.Rect
47 o := s.mon.Output()
48 s.placed, usable = arrangeLayers(o.W, o.H, s.layers)
49 s.mon.SetUsable(usable)
50}
51
52// order sorts screens by config order, then connection order, and assigns x.
53func (c *Core) order() {
54 rank := func(s *screen) int {
55 for i, o := range c.cfg.Outputs {
56 if o.Name == s.name() {
57 return i
58 }
59 }
60 return len(c.cfg.Outputs)
61 }
62 focused := c.cur()
63 slices.SortStableFunc(c.screens, func(a, b *screen) int { return rank(a) - rank(b) })
64 c.focusScreen = slices.Index(c.screens, focused)
65 // Explicitly placed outputs reserve their position first; auto-placed
66 // outputs follow the rightmost explicit edge in configuration order.
67 x := 0
68 for _, s := range c.screens {
69 for _, o := range c.cfg.Outputs {
70 if o.Name == s.name() && o.Pos != nil {
71 s.x, s.y = o.Pos.X, o.Pos.Y
72 x = max(x, s.x+s.mon.Output().W)
73 break
74 }
75 }
76 }
77 for _, s := range c.screens {
78 explicit := false
79 for _, o := range c.cfg.Outputs {
80 if o.Name == s.name() && o.Pos != nil {
81 explicit = true
82 break
83 }
84 }
85 if !explicit {
86 s.x, s.y = x, 0
87 x += s.mon.Output().W
88 }
89 }
90}
91
92// layout lists the connected outputs; the placeholder is not one.
93func (c *Core) layout() ports.Layout {
94 l := make(ports.Layout, 0, len(c.screens))
95 for _, s := range c.screens {
96 if s.name() != "" {
97 l = append(l, s.placement())
98 }
99 }
100 return l
101}
102
103// cur is the focused screen: new windows open there and binds apply to it.
104func (c *Core) cur() *screen { return c.screens[c.focusScreen] }
105
106func (c *Core) screenIndex(name string) int {
107 return slices.IndexFunc(c.screens, func(s *screen) bool { return s.name() == name })
108}
109
110// screenOf returns the screen and workspace holding the window.
111func (c *Core) screenOf(id WindowID) (*screen, *Workspace) {
112 for _, s := range c.screens {
113 if w, _ := s.mon.find(id); w != nil {
114 return s, w
115 }
116 }
117 return nil, nil
118}
119
120// byName finds a named workspace on any screen.
121func (c *Core) byName(name string) (*screen, *Workspace) {
122 for _, s := range c.screens {
123 if w := s.mon.byName(name); w != nil {
124 return s, w
125 }
126 }
127 return nil, nil
128}
129
130// isPrimary reports output.<name>.primary.
131func (c *Core) isPrimary(name string) bool {
132 for _, o := range c.cfg.Outputs {
133 if o.Name == name && o.Primary {
134 return true
135 }
136 }
137 return false
138}
139
140// anyWindow reports whether any workspace holds a window.
141func (c *Core) anyWindow() bool {
142 for _, s := range c.screens {
143 for w := range s.mon.all() {
144 if !w.empty() {
145 return true
146 }
147 }
148 }
149 return false
150}
151
152// configScale is output.<name>.scale, else 1.
153func (c *Core) configScale(name string) float64 {
154 v := 1.0
155 for _, o := range c.cfg.Outputs {
156 if o.Name == name && o.Scale != 0 {
157 v = o.Scale
158 }
159 }
160 return SnapScale(v)
161}
162
163// applyConfigScales picks up output.<name>.scale when it changed in the
164// config, replacing any live scale-up/scale-down adjustment.
165func (c *Core) applyConfigScales() {
166 for _, s := range c.screens {
167 if v := c.configScale(s.name()); v != s.cfgScale {
168 s.cfgScale = v
169 s.setScale(v)
170 }
171 }
172 c.order()
173}
174
175// addScreen handles a connected output or a new mode for a known one. The
176// first output takes over the placeholder screen core starts with; a new
177// monitor gets its named workspaces and takes its guests back.
178func (c *Core) addScreen(info ports.OutputInfo) {
179 if i := c.screenIndex(info.Name); i >= 0 {
180 s := c.screens[i]
181 s.info = info
182 if s.mon.Key != info.Key() {
183 // Another monitor on the same connector: key rules change.
184 s.mon.Key = info.Key()
185 c.settings(s.mon)
186 }
187 s.setScale(s.scale)
188 c.order()
189 return
190 }
191 var s *screen
192 if len(c.screens) == 1 && c.screens[0].name() == "" {
193 s = c.screens[0]
194 s.info = info
195 } else {
196 s = &screen{info: info, mon: newMonitorWithIDs(info.Name, info.Key(), &c.nextWorkspaceID)}
197 c.screens = append(c.screens, s)
198 }
199 s.mon.Name, s.mon.Key = info.Name, info.Key()
200 c.settings(s.mon)
201 s.cfgScale = c.configScale(info.Name)
202 s.setScale(s.cfgScale)
203 s.primary = c.isPrimary(info.Name)
204 c.order()
205 // The primary output takes the focus while nothing is open yet
206 // (startup). Later, plugging it in never moves the user.
207 if s.primary && !c.anyWindow() {
208 c.focusScreen = slices.Index(c.screens, s)
209 }
210 c.named()
211}
212
213// removeScreen moves every workspace of an unplugged output to the focused
214// screen (another one if it was focused), numbered ones below its numbered
215// workspaces, hidden ones hidden. They remember their home and position and
216// return when it is plugged in again. The last output becomes the
217// placeholder again: it keeps its windows for the next output.
218func (c *Core) removeScreen(name string) {
219 i := c.screenIndex(name)
220 if i < 0 {
221 return
222 }
223 // A capture session's off-screen workspace of this output goes with it.
224 if c.configures.cw.sc == c.screens[i] {
225 c.configures.cw.reset()
226 }
227 if len(c.screens) == 1 {
228 c.screens[0].info = ports.OutputInfo{}
229 c.screens[0].layers = nil
230 return
231 }
232 gone := c.screens[i]
233 focused := c.cur()
234 c.screens = slices.Delete(c.screens, i, i+1)
235 c.focusScreen = max(slices.Index(c.screens, focused), 0)
236 host := c.cur()
237 for pos, w := range gone.mon.Workspaces {
238 if w.empty() && w.Name == "" {
239 continue
240 }
241 switch {
242 case host.mon.matches(w.home):
243 // A guest from the host goes home.
244 w.home = ""
245 host.mon.adopt(w, false, w.homePos)
246 continue
247 case w.home == "":
248 w.home, w.homePos = gone.info.Key(), pos
249 }
250 host.mon.adopt(w, false, len(host.mon.Workspaces))
251 }
252 for _, w := range gone.mon.hidden {
253 switch {
254 case host.mon.matches(w.home):
255 w.home = ""
256 case w.home == "":
257 w.home = gone.info.Key()
258 }
259 host.mon.adopt(w, true, 0)
260 }
261 c.order()
262}
263
264// settleGuests returns guests to their connected home monitor, at their
265// original positions. A guest on screen stays until the user leaves it
266// (golden rule: automatic events never move the user).
267func (c *Core) settleGuests() {
268 for _, home := range c.screens {
269 type guest struct {
270 w, anchor *Workspace
271 host *Monitor
272 hidden bool
273 }
274 var list []guest
275 for _, host := range c.screens {
276 if host == home {
277 continue
278 }
279 for w := range host.mon.all() {
280 if home.mon.matches(w.home) && host.mon.Current() != w {
281 list = append(list, guest{w: w, anchor: w.overviewAfter, host: host.mon, hidden: host.mon.isHidden(w)})
282 }
283 }
284 }
285 // Snapshot every anchor before take normalizes a host and clears
286 // references to numbered workspaces returning alongside named rows.
287 for _, g := range list {
288 g.host.take(g.w)
289 }
290 slices.SortStableFunc(list, func(a, b guest) int { return a.w.homePos - b.w.homePos })
291 // A monitor showing an empty workspace (just plugged in) shows
292 // its first returning workspace instead.
293 idle := home.mon.Current().empty() && home.mon.Current().Name == ""
294 for _, g := range list {
295 g.w.home = ""
296 home.mon.adopt(g.w, g.hidden, g.w.homePos)
297 if idle && !g.hidden {
298 home.mon.show(g.w)
299 idle = false
300 }
301 }
302 for _, g := range list {
303 if g.hidden && indexOf(home.mon.Workspaces, g.anchor) >= 0 {
304 g.w.overviewAfter = g.anchor
305 }
306 }
307 }
308}
309
310// named applies the named workspaces from config. One that exists stays on
311// its screen; a new one is created on its home monitor, or on the first
312// screen as a guest when its home is not connected.
313func (c *Core) named() {
314 target := map[string]*screen{}
315 for _, spec := range c.specs {
316 if s, _ := c.byName(spec.Name); s != nil {
317 target[spec.Name] = s
318 continue
319 }
320 target[spec.Name] = c.screens[0]
321 for _, s := range c.screens {
322 if s.mon.matches(spec.Monitor) {
323 target[spec.Name] = s
324 }
325 }
326 }
327 for _, s := range c.screens {
328 var mine []NamedWorkspace
329 for _, spec := range c.specs {
330 if target[spec.Name] == s {
331 mine = append(mine, spec)
332 }
333 }
334 s.mon.SetNamed(mine)
335 s.mon.useSpecs(c.specs)
336 }
337 for _, spec := range c.specs {
338 s, w := c.byName(spec.Name)
339 w.home = ""
340 if spec.Monitor != "" && !s.mon.matches(spec.Monitor) {
341 w.home = spec.Monitor
342 }
343 }
344}
345
346// bringNamed prepares a "workspace <name>" toggle from the focused screen.
347// The workspace goes to its home monitor when connected, else to the focused
348// screen; that screen takes the focus. It reports true when the workspace
349// is already on screen there after a focus change: reaching it must not
350// toggle it away.
351func (c *Core) bringNamed(name string) bool {
352 src, w := c.byName(name)
353 i := slices.IndexFunc(c.specs, func(s NamedWorkspace) bool { return s.Name == name })
354 if w == nil || i < 0 {
355 return false
356 }
357 to, home := c.focusScreen, c.specs[i].Monitor
358 if h := slices.IndexFunc(c.screens, func(s *screen) bool { return s.mon.matches(home) }); h >= 0 {
359 to, home = h, ""
360 }
361 dst := c.screens[to]
362 if to != c.focusScreen {
363 c.focusScreen = to
364 if src == dst && dst.mon.Current() == w {
365 return true
366 }
367 }
368 if src != dst {
369 moveNamed(src, dst, w, home)
370 }
371 return false
372}
373
374// moveNamed moves the named workspace w from src to dst, without changing
375// what dst shows. home is its new w.home: a guest keeps waiting for its
376// home; any other forgets the output it was unplugged from.
377func moveNamed(src, dst *screen, w *Workspace, home string) {
378 hidden := src.mon.isHidden(w)
379 src.mon.take(w)
380 w.home = home
381 dst.mon.adopt(w, hidden, len(dst.mon.Workspaces))
382}
383
384// settings applies output-wide config to a monitor.
385func (c *Core) settings(m *Monitor) {
386 rules := c.cfg.Layout.LayoutRules
387 for _, o := range c.cfg.Layout.Outputs {
388 if m.matches(o.Output) {
389 rules = o.Over(rules)
390 }
391 }
392 m.SetOverflow(Overflow(rules.Overflow))
393 m.SetMaxColumns(rules.MaxColumns)
394 m.SetPresets(c.presets)
395 m.SetGaps(c.cfg.Layout.Gaps)
396 m.SetBorder(c.cfg.Border.Width)
397 m.SetStash(c.cfg.Stash.Width, c.cfg.Stash.Gap)
398 m.SetStashCapture(c.cfg.Stash.Capture)
399 m.SetFollowMove(c.cfg.Focus.FollowMove)
400}
401
402// neighbor returns the screen index left (-1) or right (+1) of the focused
403// one, or -1 at the edge.
404func (c *Core) neighbor(dir int) int {
405 i := c.focusScreen + dir
406 if i < 0 || i >= len(c.screens) {
407 return -1
408 }
409 return i
410}
411
412// moveWorkspace moves the workspace on screen to the neighbor screen, where
413// it is appended and shown; it becomes its new home (ADR 011).
414func (c *Core) moveWorkspace(dir int) {
415 to := c.neighbor(dir)
416 if to < 0 {
417 return
418 }
419 from := c.cur()
420 w := from.mon.Current()
421 if w.empty() && w.Name == "" {
422 return
423 }
424 hidden := from.mon.isHidden(w)
425 from.mon.take(w)
426 w.home = ""
427 dst := c.screens[to]
428 dst.mon.adopt(w, hidden, len(dst.mon.Workspaces))
429 dst.mon.show(w)
430 c.focusScreen = to
431}