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 ordered by config
11// (output.<name>.*), then by connection; order places them (placeOutputs).
12type screen struct {
13 info ports.OutputInfo
14 mon *Monitor
15 scale, cfgScale float64
16 // transform is how the physical target holds the screen's logical
17 // output (output.<name>.transform); 90/270 swap width and height.
18 transform ports.BufferTransform
19 primary bool
20 x, y int // logical origin in the global layout
21 off bool // turned off by a client (output power management)
22 layers []ports.LayerSurface
23 placed []ports.SceneLayer
24 // capture holds the attached layers of the capture exclusion: shown over
25 // a fullscreen window (capture.go).
26 capture map[WindowID]bool
27 // capScene and capMarks are what the last scene carried (Capture and
28 // CaptureIndicators): immutable, shared by the next one while unchanged.
29 capScene *ports.SceneCapture
30 capMarks []ports.CaptureIndicator
31 // last is the scene published for this screen: an identical next one
32 // keeps its Seq (Scene.SameAs). Zero after a protected publish.
33 last ports.Scene
34 // settledLayout is the monitor's layout as of the last publish: what
35 // configures are sized from. shown is the same with the rect motions applied: what is
36 // drawn and hit-tested. They are the same slice while no rect motion
37 // runs. Both are built once per publish (refreshShown); shown is nil
38 // before the first publish, then callers measure the monitor's layout.
39 settledLayout, shown []Placement
40 // layoutBuf backs settledLayout (and shown, while no rect motion runs),
41 // rebuilt in place by every refreshShown; shownBuf and settledBuf back
42 // shown and settledLayout while rect motions run. All three are owner
43 // goroutine scratch reused across publishes: a Scene never aliases them
44 // (its windows are copied from the placements), and a caller that
45 // keeps a layout past the next publish copies it (Layout() is fresh).
46 layoutBuf, shownBuf, settledBuf []Placement
47 // leaveIDs is withLeaving's scratch list.
48 leaveIDs []WindowID
49 // rects are the running per-window rect motions (transition.go) of
50 // rectsWS, the workspace they began on (nil: the overview); created
51 // lazily.
52 rects map[WindowID]rectMotion
53 rectsWS *Workspace
54}
55
56// springing reports whether a camera or rect spring runs on the screen.
57func (s *screen) springing() bool { return s.mon.springing() || len(s.rects) > 0 }
58
59// stopRects drops the rect motions: the windows are drawn where they are.
60func (s *screen) stopRects() { clear(s.rects); s.rectsWS = nil }
61
62// rectsOwner is what the screen's rect motions belong to: its workspace on
63// screen, or nil in a preview (the overview or the column switcher), where the
64// workspace changes under the cards.
65func (s *screen) rectsOwner() *Workspace {
66 if s.mon.previewing() {
67 return nil
68 }
69 return s.mon.Current()
70}
71
72// shownLayout is the layout as drawn, for hit-testing and popups; before
73// the first publish it is the monitor's.
74func (s *screen) shownLayout() []Placement {
75 if s.shown != nil {
76 return s.shown
77 }
78 return s.mon.Layout()
79}
80
81func (s *screen) name() string { return s.info.Name }
82
83// modeSize is the physical mode size in the screen's logical orientation:
84// width and height swap for a 90/270 transform.
85func (s *screen) modeSize() (w, h int) {
86 return s.transform.Size(s.info.Width, s.info.Height)
87}
88
89// rect is the screen's place in the global logical layout.
90func (s *screen) rect() Rect {
91 o := s.mon.Output()
92 return Rect{X: s.x, Y: s.y, W: o.W, H: o.H}
93}
94
95func (s *screen) placement() ports.OutputPlacement {
96 r := s.rect()
97 return ports.OutputPlacement{Info: s.info, X: r.X, Y: r.Y, Width: r.W, Height: r.H, Scale: s.scale, Transform: s.transform, Primary: s.primary}
98}
99
100// setScale resizes the logical output; layer placement follows.
101func (s *screen) setScale(v float64) {
102 s.scale = SnapScale(v)
103 w, h := s.modeSize()
104 s.mon.SetOutput(logical(w, s.scale), logical(h, s.scale))
105 s.arrange()
106}
107
108// arrange places the layer surfaces and gives workspaces the usable area.
109func (s *screen) arrange() {
110 var usable ports.Rect
111 o := s.mon.Output()
112 s.placed, usable = arrangeLayers(o.W, o.H, s.layers)
113 s.mon.SetUsable(usable)
114}
115
116// order sorts screens by config order, then connection order, and places them
117// in the global layout (placeOutputs).
118func (c *Core) order() {
119 rank := func(s *screen) int {
120 if i := c.outputIndex(s.name()); i >= 0 {
121 return i
122 }
123 return len(c.cfg.Outputs)
124 }
125 focused := c.cur()
126 slices.SortStableFunc(c.screens, func(a, b *screen) int { return rank(a) - rank(b) })
127 c.focusScreen = slices.Index(c.screens, focused)
128 items := make([]placeItem, len(c.screens))
129 for i, s := range c.screens {
130 r := s.rect()
131 items[i].name, items[i].w, items[i].h = s.name(), r.W, r.H
132 if cfg, ok := c.outputConfig(s.name()); ok {
133 items[i].pos, items[i].anchor = cfg.Pos, cfg.Anchor
134 }
135 }
136 for i, p := range placeOutputs(items) {
137 c.screens[i].x, c.screens[i].y = p.X, p.Y
138 }
139}
140
141// outputIndex is the position of the output.<name> entry in the config, or
142// -1; names are unique in the config.
143func (c *Core) outputIndex(name string) int {
144 return slices.IndexFunc(c.cfg.Outputs, func(o ports.OutputConfig) bool { return o.Name == name })
145}
146
147// outputConfig returns the output.<name> entry.
148func (c *Core) outputConfig(name string) (ports.OutputConfig, bool) {
149 i := c.outputIndex(name)
150 if i < 0 {
151 return ports.OutputConfig{}, false
152 }
153 return c.cfg.Outputs[i], true
154}
155
156// layout lists the connected outputs; the placeholder is not one.
157func (c *Core) layout() ports.Layout { return c.layoutInto(make(ports.Layout, 0, len(c.screens))) }
158
159// layoutInto is layout built in l[:0]: the result aliases l's storage.
160func (c *Core) layoutInto(l ports.Layout) ports.Layout {
161 l = l[:0]
162 for _, s := range c.screens {
163 if s.name() != "" {
164 l = append(l, s.placement())
165 }
166 }
167 return l
168}
169
170// cur is the focused screen: new windows open there and binds apply to it.
171func (c *Core) cur() *screen { return c.screens[c.focusScreen] }
172
173func (c *Core) screenIndex(name string) int {
174 return slices.IndexFunc(c.screens, func(s *screen) bool { return s.name() == name })
175}
176
177// screenAt is the index of the first connected screen containing the global
178// point, or -1. It does not allocate.
179func (c *Core) screenAt(x, y float64) int {
180 for i, s := range c.screens {
181 if s.name() == "" {
182 continue
183 }
184 r := s.rect()
185 if x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) {
186 return i
187 }
188 }
189 return -1
190}
191
192// clampPointer keeps the pointer on a screen: a point outside every screen
193// stays on the screen under (fromX, fromY), or the first one, clamped to its
194// edges.
195func (c *Core) clampPointer(fromX, fromY, x, y float64) (float64, float64) {
196 i := c.screenAt(x, y)
197 if i >= 0 {
198 return x, y
199 }
200 if i = c.screenAt(fromX, fromY); i < 0 {
201 i = slices.IndexFunc(c.screens, func(s *screen) bool { return s.name() != "" })
202 if i < 0 {
203 return x, y
204 }
205 }
206 r := c.screens[i].rect()
207 return min(max(x, float64(r.X)), float64(r.X+r.W)-1), min(max(y, float64(r.Y)), float64(r.Y+r.H)-1)
208}
209
210// screenOf returns the screen and workspace holding the window.
211func (c *Core) screenOf(id WindowID) (*screen, *Workspace) {
212 for _, s := range c.screens {
213 if w, _ := s.mon.find(id); w != nil {
214 return s, w
215 }
216 }
217 return nil, nil
218}
219
220// byName finds a named workspace on any screen.
221func (c *Core) byName(name string) (*screen, *Workspace) {
222 for _, s := range c.screens {
223 if w := s.mon.byName(name); w != nil {
224 return s, w
225 }
226 }
227 return nil, nil
228}
229
230// isPrimary reports output.<name>.primary.
231func (c *Core) isPrimary(name string) bool {
232 o, _ := c.outputConfig(name)
233 return o.Primary
234}
235
236// anyWindow reports whether any workspace holds a window.
237func (c *Core) anyWindow() bool {
238 for _, s := range c.screens {
239 for w := range s.mon.all() {
240 if !w.empty() {
241 return true
242 }
243 }
244 }
245 return false
246}
247
248// configScale is output.<name>.scale, else 1.
249func (c *Core) configScale(name string) float64 {
250 v := 1.0
251 if o, ok := c.outputConfig(name); ok && o.Scale != 0 {
252 v = o.Scale
253 }
254 return SnapScale(v)
255}
256
257// configTransform is output.<name>.transform, else 0.
258func (c *Core) configTransform(name string) ports.BufferTransform {
259 o, _ := c.outputConfig(name)
260 return o.Transform
261}
262
263// applyOutputConfig picks up output.<name>.scale and .transform when they
264// changed in the config. A new scale replaces any live scale-up/scale-down
265// adjustment; a transform alone keeps it.
266func (c *Core) applyOutputConfig() {
267 for _, s := range c.screens {
268 v, t := c.configScale(s.name()), c.configTransform(s.name())
269 if v == s.cfgScale && t == s.transform {
270 continue
271 }
272 scale := s.scale
273 if v != s.cfgScale {
274 scale = v
275 }
276 s.cfgScale, s.transform = v, t
277 s.setScale(scale)
278 }
279 c.order()
280}
281
282// addScreen handles a connected output or a new mode for a known one. The
283// first output takes over the placeholder screen core starts with; a new
284// monitor gets its named workspaces and takes its guests back.
285func (c *Core) addScreen(info ports.OutputInfo) {
286 if i := c.screenIndex(info.Name); i >= 0 {
287 s := c.screens[i]
288 s.info = info
289 if s.mon.Key != info.Key() {
290 // Another monitor on the same connector: key rules change.
291 s.mon.Key = info.Key()
292 c.settings(s.mon)
293 }
294 s.transform = c.configTransform(info.Name)
295 s.setScale(s.scale)
296 c.order()
297 return
298 }
299 var s *screen
300 if len(c.screens) == 1 && c.screens[0].name() == "" {
301 s = c.screens[0]
302 s.info = info
303 } else {
304 s = &screen{info: info, mon: newMonitorWithIDs(info.Name, info.Key(), &c.nextWorkspaceID)}
305 c.screens = append(c.screens, s)
306 }
307 s.off = c.offGone[info.Name]
308 delete(c.offGone, info.Name)
309 s.mon.Name, s.mon.Key = info.Name, info.Key()
310 c.settings(s.mon)
311 s.cfgScale = c.configScale(info.Name)
312 s.transform = c.configTransform(info.Name)
313 s.setScale(s.cfgScale)
314 s.primary = c.isPrimary(info.Name)
315 c.order()
316 // The primary output takes the focus while nothing is open yet
317 // (startup). Later, plugging it in never moves the user.
318 if s.primary && !c.anyWindow() {
319 c.focusScreen = slices.Index(c.screens, s)
320 }
321 c.named()
322}
323
324// removeScreen moves every workspace of an unplugged output to the focused
325// screen (another one if it was focused), numbered ones below its numbered
326// workspaces, hidden ones hidden. They remember their home and position and
327// return when it is plugged in again. The last output becomes the
328// placeholder again: it keeps its windows for the next output.
329func (c *Core) removeScreen(name string) {
330 i := c.screenIndex(name)
331 if i < 0 {
332 return
333 }
334 if c.switcher.sc == c.screens[i] {
335 c.cancelSwitcher()
336 }
337 // A capture session's off-screen workspace of this output goes with it.
338 if c.configures.cw.sc == c.screens[i] {
339 c.configures.cw.reset()
340 }
341 if c.screens[i].off {
342 if c.offGone == nil {
343 c.offGone = map[string]bool{}
344 }
345 c.offGone[name] = true
346 }
347 if len(c.screens) == 1 {
348 c.screens[0].info = ports.OutputInfo{}
349 c.screens[0].layers = nil
350 // Nothing draws the placeholder: its springs would only wake the
351 // fallback timer, and start from the old output's geometry if an
352 // output came back.
353 c.screens[0].stopAnimations()
354 return
355 }
356 gone := c.screens[i]
357 if c.swipe != nil && c.swipe.screen == gone {
358 // Its slide would stay on the workspaces the host adopts.
359 c.dropSwipe()
360 c.swipe = nil
361 }
362 focused := c.cur()
363 c.screens = slices.Delete(c.screens, i, i+1)
364 c.focusScreen = max(slices.Index(c.screens, focused), 0)
365 host := c.cur()
366 for pos, w := range gone.mon.Workspaces {
367 if w.empty() && w.Name == "" {
368 continue
369 }
370 switch {
371 case host.mon.matches(w.home):
372 // A guest from the host goes home.
373 w.home = ""
374 host.mon.adopt(w, false, w.homePos)
375 continue
376 case w.home == "":
377 w.home, w.homePos = gone.info.Key(), pos
378 }
379 host.mon.adopt(w, false, len(host.mon.Workspaces))
380 }
381 for _, w := range gone.mon.hidden {
382 switch {
383 case host.mon.matches(w.home):
384 w.home = ""
385 case w.home == "":
386 w.home = gone.info.Key()
387 }
388 host.mon.adopt(w, true, 0)
389 }
390 c.order()
391}
392
393// settleGuests returns guests to their connected home monitor, at their
394// original positions. A guest on screen stays until the user leaves it
395// (golden rule: automatic events never move the user).
396func (c *Core) settleGuests() {
397 for _, home := range c.screens {
398 type guest struct {
399 w, anchor *Workspace
400 host *Monitor
401 hidden bool
402 }
403 var list []guest
404 for _, host := range c.screens {
405 if host == home {
406 continue
407 }
408 for w := range host.mon.all() {
409 if home.mon.matches(w.home) && host.mon.Current() != w {
410 list = append(list, guest{w: w, anchor: w.overviewAfter, host: host.mon, hidden: host.mon.isHidden(w)})
411 }
412 }
413 }
414 // Snapshot every anchor before take normalizes a host and clears
415 // references to numbered workspaces returning alongside named rows.
416 for _, g := range list {
417 g.host.take(g.w)
418 }
419 slices.SortStableFunc(list, func(a, b guest) int { return a.w.homePos - b.w.homePos })
420 // A monitor showing an empty workspace (just plugged in) shows
421 // its first returning workspace instead.
422 idle := home.mon.Current().empty() && home.mon.Current().Name == ""
423 for _, g := range list {
424 g.w.home = ""
425 home.mon.adopt(g.w, g.hidden, g.w.homePos)
426 if idle && !g.hidden {
427 home.mon.show(g.w)
428 idle = false
429 }
430 }
431 for _, g := range list {
432 if g.hidden && indexOf(home.mon.Workspaces, g.anchor) >= 0 {
433 g.w.overviewAfter = g.anchor
434 }
435 }
436 }
437}
438
439// named applies the named workspaces from config. One that exists stays on
440// its screen; a new one is created on its home monitor, or on the first
441// screen as a guest when its home is not connected.
442func (c *Core) named() {
443 target := map[string]*screen{}
444 for _, spec := range c.specs {
445 if s, _ := c.byName(spec.Name); s != nil {
446 target[spec.Name] = s
447 continue
448 }
449 target[spec.Name] = c.screens[0]
450 for _, s := range c.screens {
451 if s.mon.matches(spec.Monitor) {
452 target[spec.Name] = s
453 }
454 }
455 }
456 for _, s := range c.screens {
457 var mine []NamedWorkspace
458 for _, spec := range c.specs {
459 if target[spec.Name] == s {
460 mine = append(mine, spec)
461 }
462 }
463 s.mon.SetNamed(mine)
464 s.mon.useSpecs(c.specs)
465 }
466 for _, spec := range c.specs {
467 s, w := c.byName(spec.Name)
468 w.home = ""
469 if spec.Monitor != "" && !s.mon.matches(spec.Monitor) {
470 w.home = spec.Monitor
471 }
472 }
473}
474
475// bringNamed prepares a "workspace <name>" toggle from the focused screen.
476// The workspace goes to its home monitor when connected, else to the focused
477// screen; that screen takes the focus. It reports true when the workspace
478// is already on screen there after a focus change: reaching it must not
479// toggle it away.
480func (c *Core) bringNamed(name string) bool {
481 src, w := c.byName(name)
482 i := slices.IndexFunc(c.specs, func(s NamedWorkspace) bool { return s.Name == name })
483 if w == nil || i < 0 {
484 return false
485 }
486 to, home := c.focusScreen, c.specs[i].Monitor
487 if h := slices.IndexFunc(c.screens, func(s *screen) bool { return s.mon.matches(home) }); h >= 0 {
488 to, home = h, ""
489 }
490 dst := c.screens[to]
491 if to != c.focusScreen {
492 c.focusScreen = to
493 if src == dst && dst.mon.Current() == w {
494 return true
495 }
496 }
497 if src != dst {
498 moveNamed(src, dst, w, home)
499 }
500 return false
501}
502
503// moveNamed moves the named workspace w from src to dst, without changing
504// what dst shows. home is its new w.home: a guest keeps waiting for its
505// home; any other forgets the output it was unplugged from.
506func moveNamed(src, dst *screen, w *Workspace, home string) {
507 hidden := src.mon.isHidden(w)
508 src.mon.take(w)
509 w.home = home
510 dst.mon.adopt(w, hidden, len(dst.mon.Workspaces))
511}
512
513// settings applies output-wide config to a monitor.
514func (c *Core) settings(m *Monitor) {
515 rules := c.cfg.Layout.LayoutRules
516 for _, o := range c.cfg.Layout.Outputs {
517 if m.matches(o.Output) {
518 rules = o.Over(rules)
519 }
520 }
521 m.SetOverflow(Overflow(rules.Overflow))
522 m.SetMaxColumns(rules.MaxColumns)
523 m.SetGaps(c.cfg.Layout.Gaps)
524 m.SetBorder(c.cfg.Border.Width)
525 m.SetStash(c.cfg.Stash.Width, c.cfg.Stash.Gap)
526 m.SetStashCapture(c.cfg.Stash.Capture)
527 m.SetStashTerminal(c.cfg.Stash.SpawnTerminal && len(c.cfg.Terminal.Command) > 0)
528 m.SetFollowMove(c.cfg.Focus.FollowMove)
529}
530
531// direction is where a monitor action looks for a neighbor screen.
532type direction uint8
533
534const (
535 dirLeft direction = iota + 1
536 dirRight
537 dirUp
538 dirDown
539)
540
541// neighbor returns the index of the screen next to the focused one in
542// direction d, or -1 at the edge. Candidates lie fully beyond the focused
543// screen's edge; one sharing part of that edge wins over a diagonal one, then
544// the smallest gap, the largest shared edge, the closest centers and the
545// lowest index decide. Overlapping outputs (for instance two anchored to the
546// same reference) are reached only when no screen lies fully beyond the edge
547// in that direction: a second pass then takes the screens whose rectangle
548// intersects the focused one and whose center is strictly beyond the focused
549// center, ranked the same way with a gap of 0. A screen that does have a
550// strict candidate never falls back, so an overlapping one can stay out of
551// reach. Screens with the same center on the axis are unreachable by direction
552// (the pointer still reaches them). There is no wrap.
553func (c *Core) neighbor(d direction) int {
554 if d < dirLeft || d > dirDown {
555 return -1
556 }
557 // span is the extent of s along the move (lo, hi) and across it.
558 span := func(s *screen) (lo, hi, clo, chi int) {
559 r := s.rect()
560 if d == dirUp || d == dirDown {
561 return r.Y, r.Y + r.H, r.X, r.X + r.W
562 }
563 return r.X, r.X + r.W, r.Y, r.Y + r.H
564 }
565 lo, hi, clo, chi := span(c.cur())
566 pick := func(fallback bool) int {
567 best, bestKey := -1, [4]int{}
568 for i, s := range c.screens {
569 if i == c.focusScreen || s.name() == "" {
570 continue
571 }
572 l, h, cl, ch := span(s)
573 gap := l - hi
574 beyond := l+h > lo+hi // centers, doubled
575 if d == dirLeft || d == dirUp {
576 gap = lo - h
577 beyond = l+h < lo+hi
578 }
579 if fallback {
580 if !beyond || min(hi, h)-max(lo, l) <= 0 || min(chi, ch)-max(clo, cl) <= 0 {
581 continue
582 }
583 gap = 0
584 } else if gap < 0 {
585 continue
586 }
587 overlap := min(chi, ch) - max(clo, cl)
588 diagonal := 0
589 if overlap <= 0 {
590 diagonal = 1
591 }
592 key := [4]int{diagonal, gap, -max(overlap, 0), abs(cl + ch - clo - chi)}
593 if best < 0 || slices.Compare(key[:], bestKey[:]) < 0 {
594 best, bestKey = i, key
595 }
596 }
597 return best
598 }
599 if i := pick(false); i >= 0 {
600 return i
601 }
602 return pick(true)
603}
604
605// moveWorkspace moves the workspace on screen to the neighbor screen, where
606// it is appended and shown; it becomes its new home (ADR 011).
607func (c *Core) moveWorkspace(d direction) {
608 to := c.neighbor(d)
609 if to < 0 {
610 return
611 }
612 from := c.cur()
613 w := from.mon.Current()
614 if w.empty() && w.Name == "" {
615 return
616 }
617 hidden := from.mon.isHidden(w)
618 from.mon.take(w)
619 w.home = ""
620 dst := c.screens[to]
621 dst.mon.adopt(w, hidden, len(dst.mon.Workspaces))
622 dst.mon.show(w)
623 c.focusScreen = to
624}