1package core
2
3import "github.com/bnema/neferwl/internal/ports"
4
5// configureTarget is where a placement is shown, as the configure needs it.
6type configureTarget struct {
7 output string
8 area Rect // the output, to tell whether the window is on screen
9 focused bool // activated: focused on the focused output
10 // client is the placement minus borders; imposed reports core sized a
11 // floating window (native floats pick their own size).
12 client Rect
13 imposed bool
14 // captureArea is the viewport of the workspace a capture session renders
15 // off screen, when this window belongs to it (captureConfigure).
16 captureArea Rect
17 // realTiled sizes a visible tiled preview from its real client rect.
18 realTiled bool
19 // captured: a capture session renders this window off screen (window
20 // capture), so off screen it is Captured, not suspended.
21 captured bool
22}
23
24// configures owns the last configure sent to each window and decides the
25// next one. Configures are sent only when they change.
26type configures struct {
27 sent map[WindowID]ports.ConfigureWindow
28 seen map[WindowID]bool
29 // answer holds windows whose state request awaits a configure, sent
30 // even unchanged: xdg-shell answers every request, a refused one too.
31 answer map[WindowID]bool
32 // cw is the workspace a capture session renders off screen, if any.
33 cw captureWorkspace
34}
35
36func newConfigures() configures {
37 return configures{sent: map[WindowID]ports.ConfigureWindow{}, seen: map[WindowID]bool{}, answer: map[WindowID]bool{}}
38}
39
40// next returns the configure for p at t and whether it must be sent. The
41// caller records it with mark once sent.
42func (s *configures) next(p Placement, t configureTarget) (ports.ConfigureWindow, bool) {
43 s.seen[p.ID] = true
44 old, ok := s.sent[p.ID]
45 v := build(p, t, old, ok)
46 v.Captured = t.captured && !v.Visible
47 if t.captured && p.Hidden && t.client.W > 0 && t.client.H > 0 {
48 // Hidden but captured: sized for the capture, which follows its real
49 // layout.
50 v.Width, v.Height = t.client.W, t.client.H
51 }
52 return v, !ok || v != old || s.answer[p.ID]
53}
54
55// nextWithCapture is next for a window of a workspace a capture session
56// renders off screen. p is its placement on the physical layout; cp is its
57// real placement on its own workspace, laid out in the viewport
58// t.captureArea (nil: not captured, exactly next).
59//
60// A hidden p is sized from cp like a shown window and stays physically
61// invisible and deactivated (Visible false). Any other p (an overview
62// preview) is configured as usual, keeping its real size; only when the
63// display does not show it does it get Captured, so a capture that draws it
64// does not see it suspended. Captured is set when cp is drawn in the
65// viewport.
66func (s *configures) nextWithCapture(p Placement, t configureTarget, cp *Placement) (ports.ConfigureWindow, bool) {
67 if cp == nil {
68 return s.next(p, t)
69 }
70 s.seen[p.ID] = true
71 old, ok := s.sent[p.ID]
72 var v ports.ConfigureWindow
73 if p.Hidden {
74 t.focused = false
75 v = build(*cp, t, old, ok)
76 v.Activated, v.Visible = false, false
77 } else {
78 v = build(p, t, old, ok)
79 }
80 v.Captured = !v.Visible && (t.captured || !cp.Hidden && cp.Rect.Overlaps(t.captureArea))
81 return v, !ok || v != old || s.answer[p.ID]
82}
83
84// build is the configure for p at t given the last one sent, if any. It
85// never sets Captured: only nextWithCapture does, so a window that leaves a
86// capture session drops the flag with its next configure.
87func build(p Placement, t configureTarget, old ports.ConfigureWindow, ok bool) ports.ConfigureWindow {
88 var v ports.ConfigureWindow
89 switch {
90 case p.Hidden:
91 // A hidden window keeps its size, state and output; it is only
92 // deactivated and marked invisible. One mapped hidden is told so
93 // at once, sized by the client.
94 v = old
95 if !ok {
96 v = ports.ConfigureWindow{ID: p.ID, Floating: p.Floating}
97 }
98 v.Activated, v.Visible, v.Output, v.Captured = false, false, t.output, false
99 case p.Preview > 0:
100 // A preview keeps its client's size: only its focus changes.
101 v = old
102 if !ok {
103 v = ports.ConfigureWindow{ID: p.ID, Width: int(float64(p.Rect.W) / p.Preview), Height: int(float64(p.Rect.H) / p.Preview), Floating: p.Floating}
104 if p.Floating {
105 // A native float picks its own size.
106 v.Width, v.Height = 0, 0
107 }
108 }
109 if p.Fullscreen && !v.Fullscreen {
110 // A float made fullscreen off screen: its preview row shows
111 // it fullscreen, sized for the output, never the preview.
112 v.Width, v.Height, v.Fullscreen, v.Floating = t.area.W, t.area.H, true, false
113 }
114 if t.realTiled {
115 v.Width, v.Height = t.client.W, t.client.H
116 }
117 v.Activated, v.Visible, v.Output, v.Captured = t.focused, onScreen(p, t.area), t.output, false
118 default:
119 v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: onScreen(p, t.area)}
120 if v.Floating && !t.imposed {
121 v.Width, v.Height = 0, 0
122 }
123 }
124 return v
125}
126
127// mark records v as sent.
128func (s *configures) mark(v ports.ConfigureWindow) {
129 s.sent[v.ID] = v
130 delete(s.answer, v.ID)
131}
132
133// keep marks id as seen without a configure: a window drawn while it leaves
134// keeps what was sent to it. Only for a window the layout still holds (a
135// hidden stash window): a destroyed one is forgotten (forget).
136func (s *configures) keep(id WindowID) { s.seen[id] = true }
137
138// forget drops what was sent to id: the window is unmapped. A toplevel
139// that maps again keeps its ID, so it must start from nothing, or an
140// unchanged first configure would be withheld.
141func (s *configures) forget(id WindowID) {
142 delete(s.sent, id)
143 delete(s.answer, id)
144 delete(s.seen, id)
145}
146
147// prune forgets the windows no next call saw since the last prune: they
148// left the layout.
149func (s *configures) prune() {
150 for id := range s.sent {
151 if !s.seen[id] {
152 delete(s.sent, id)
153 }
154 }
155 for id := range s.answer {
156 if !s.seen[id] {
157 delete(s.answer, id)
158 }
159 }
160 clear(s.seen)
161}