1package core
2
3import (
4 "context"
5 "slices"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// popupState is a placed xdg_popup. Rect is relative to the parent's
11// window geometry, as sent to the client.
12type popupState struct {
13 id, parent WindowID
14 rect Rect
15 grab bool
16 mapped bool
17}
18
19// place computes a popup rectangle relative to its parent, kept inside
20// bounds (also parent-relative) as the positioner's adjustments allow.
21func place(p ports.Positioner, bounds Rect) Rect {
22 r := initial(p, p.Anchor, p.Gravity)
23 if bounds.W <= 0 || bounds.H <= 0 {
24 return r
25 }
26 outX := func(r Rect) bool { return r.X < bounds.X || r.X+r.W > bounds.X+bounds.W }
27 outY := func(r Rect) bool { return r.Y < bounds.Y || r.Y+r.H > bounds.Y+bounds.H }
28 if outX(r) && p.Adjust&ports.AdjustFlipX != 0 {
29 if f := initial(p, flipX(p.Anchor), flipX(p.Gravity)); !outX(f) {
30 r.X = f.X
31 }
32 }
33 if outY(r) && p.Adjust&ports.AdjustFlipY != 0 {
34 if f := initial(p, flipY(p.Anchor), flipY(p.Gravity)); !outY(f) {
35 r.Y = f.Y
36 }
37 }
38 if outX(r) && p.Adjust&ports.AdjustSlideX != 0 {
39 r.X = max(min(r.X, bounds.X+bounds.W-r.W), bounds.X)
40 }
41 if outY(r) && p.Adjust&ports.AdjustSlideY != 0 {
42 r.Y = max(min(r.Y, bounds.Y+bounds.H-r.H), bounds.Y)
43 }
44 if outX(r) && p.Adjust&ports.AdjustResizeX != 0 {
45 x0, x1 := max(r.X, bounds.X), min(r.X+r.W, bounds.X+bounds.W)
46 if x1 > x0 {
47 r.X, r.W = x0, x1-x0
48 }
49 }
50 if outY(r) && p.Adjust&ports.AdjustResizeY != 0 {
51 y0, y1 := max(r.Y, bounds.Y), min(r.Y+r.H, bounds.Y+bounds.H)
52 if y1 > y0 {
53 r.Y, r.H = y0, y1-y0
54 }
55 }
56 return r
57}
58
59// initial is the unconstrained placement for an anchor and gravity.
60func initial(p ports.Positioner, anchor, gravity uint32) Rect {
61 a := p.AnchorRect
62 x, y := a.X+a.W/2, a.Y+a.H/2
63 switch anchor {
64 case ports.EdgeLeft, ports.EdgeTopLeft, ports.EdgeBottomLeft:
65 x = a.X
66 case ports.EdgeRight, ports.EdgeTopRight, ports.EdgeBottomRight:
67 x = a.X + a.W
68 }
69 switch anchor {
70 case ports.EdgeTop, ports.EdgeTopLeft, ports.EdgeTopRight:
71 y = a.Y
72 case ports.EdgeBottom, ports.EdgeBottomLeft, ports.EdgeBottomRight:
73 y = a.Y + a.H
74 }
75 x, y = x+p.OffsetX, y+p.OffsetY
76 r := Rect{X: x - p.Width/2, Y: y - p.Height/2, W: p.Width, H: p.Height}
77 switch gravity {
78 case ports.EdgeLeft, ports.EdgeTopLeft, ports.EdgeBottomLeft:
79 r.X = x - p.Width
80 case ports.EdgeRight, ports.EdgeTopRight, ports.EdgeBottomRight:
81 r.X = x
82 }
83 switch gravity {
84 case ports.EdgeTop, ports.EdgeTopLeft, ports.EdgeTopRight:
85 r.Y = y - p.Height
86 case ports.EdgeBottom, ports.EdgeBottomLeft, ports.EdgeBottomRight:
87 r.Y = y
88 }
89 return r
90}
91
92func flipX(e uint32) uint32 {
93 switch e {
94 case ports.EdgeLeft:
95 return ports.EdgeRight
96 case ports.EdgeRight:
97 return ports.EdgeLeft
98 case ports.EdgeTopLeft:
99 return ports.EdgeTopRight
100 case ports.EdgeTopRight:
101 return ports.EdgeTopLeft
102 case ports.EdgeBottomLeft:
103 return ports.EdgeBottomRight
104 case ports.EdgeBottomRight:
105 return ports.EdgeBottomLeft
106 }
107 return e
108}
109
110func flipY(e uint32) uint32 {
111 switch e {
112 case ports.EdgeTop:
113 return ports.EdgeBottom
114 case ports.EdgeBottom:
115 return ports.EdgeTop
116 case ports.EdgeTopLeft:
117 return ports.EdgeBottomLeft
118 case ports.EdgeBottomLeft:
119 return ports.EdgeTopLeft
120 case ports.EdgeTopRight:
121 return ports.EdgeBottomRight
122 case ports.EdgeBottomRight:
123 return ports.EdgeTopRight
124 }
125 return e
126}
127
128// windowRect is where a window, layer surface or popup is on its screen: its client
129// area, output-local and logical. ok is false when it is not on screen.
130func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) {
131 if p := c.popups[id]; p != nil {
132 sc, pr, ok := c.windowRect(p.parent)
133 // A peeking stashed window shows no popups: they would cover the
134 // selected one.
135 if !ok || c.peeking(p.parent) {
136 return nil, Rect{}, false
137 }
138 return sc, Rect{X: pr.X + p.rect.X, Y: pr.Y + p.rect.Y, W: p.rect.W, H: p.rect.H}, true
139 }
140 if sc, l, ok := c.layerOf(id); ok {
141 if !shown(sc, l) {
142 return nil, Rect{}, false
143 }
144 return sc, l.Rect, true
145 }
146 for _, sc := range c.screens {
147 for _, pl := range sc.shownLayout() {
148 if pl.ID == id {
149 // Hidden or scrolled off: not on screen, like its popups.
150 // A preview takes no input and shows no popups.
151 if !onScreen(pl, sc.mon.Frame()) || pl.Preview > 0 {
152 return nil, Rect{}, false
153 }
154 return sc, c.clientRect(pl), true
155 }
156 }
157 }
158 return nil, Rect{}, false
159}
160
161// peeking reports whether id is a stashed window peeking in on screen.
162func (c *Core) peeking(id WindowID) bool {
163 for _, sc := range c.screens {
164 for _, pl := range sc.shownLayout() {
165 if pl.ID == id {
166 return pl.Peek
167 }
168 }
169 }
170 return false
171}
172
173// placePopup places a new or repositioned popup and configures it; a popup
174// whose parent is not on screen is dismissed.
175func (c *Core) placePopup(ctx context.Context, v ports.PopupRequest) error {
176 sc, pr, physical, ok := c.popupRect(v.Parent)
177 if !ok {
178 if c.popups[v.ID] != nil {
179 return c.closePopup(ctx, v.ID)
180 }
181 return c.command(ctx, ports.ClosePopup{ID: v.ID})
182 }
183 // Window popups stay in the workspace viewport, layer popups in the output.
184 o := sc.mon.Output()
185 if !c.onLayer(c.popupRoot(v.Parent)) {
186 o = sc.mon.Frame()
187 if !physical {
188 o = c.configures.cw.frame // the captured workspace's viewport
189 }
190 }
191 bounds := Rect{X: o.X - pr.X, Y: o.Y - pr.Y, W: o.W, H: o.H}
192 p := c.popups[v.ID]
193 if p == nil {
194 p = &popupState{id: v.ID, parent: v.Parent}
195 c.popups[v.ID] = p
196 c.popupOrder = append(c.popupOrder, v.ID)
197 }
198 p.grab = v.Grab
199 p.rect = place(v.Positioner, bounds)
200 return c.command(ctx, ports.ConfigurePopup{ID: v.ID, Rect: p.rect, Reposition: v.Reposition, Token: v.Token})
201}
202
203// closePopup dismisses a popup after its children, topmost first as
204// xdg-shell requires, and forgets them.
205func (c *Core) closePopup(ctx context.Context, id WindowID) error {
206 if err := c.closePopupsOf(ctx, id); err != nil {
207 return err
208 }
209 if err := c.command(ctx, ports.ClosePopup{ID: id}); err != nil {
210 return err
211 }
212 c.forgetPopup(id)
213 return nil
214}
215
216// closePopupsOf dismisses the popups hanging from a window or popup,
217// newest first.
218func (c *Core) closePopupsOf(ctx context.Context, id WindowID) error {
219 for _, child := range slices.Backward(slices.Clone(c.popupOrder)) {
220 if p := c.popups[child]; p != nil && p.parent == id {
221 if err := c.closePopup(ctx, child); err != nil {
222 return err
223 }
224 }
225 }
226 return nil
227}
228
229// dropPopup forgets a popup the client unmapped and dismisses its children.
230func (c *Core) dropPopup(ctx context.Context, id WindowID) error {
231 if c.popups[id] == nil {
232 return nil
233 }
234 if err := c.closePopupsOf(ctx, id); err != nil {
235 return err
236 }
237 c.forgetPopup(id)
238 return nil
239}
240
241func (c *Core) forgetPopup(id WindowID) {
242 delete(c.popups, id)
243 c.popupOrder = slices.DeleteFunc(c.popupOrder, func(v WindowID) bool { return v == id })
244 if c.pointer == id {
245 c.pointer = 0
246 }
247}
248
249// closeHiddenPopups dismisses popups whose window left the screen.
250func (c *Core) closeHiddenPopups(ctx context.Context) error {
251 for _, id := range slices.Backward(slices.Clone(c.popupOrder)) {
252 if c.popups[id] == nil {
253 continue
254 }
255 // A popup of a window a capture session renders off screen stays.
256 if _, _, _, ok := c.popupRect(id); !ok {
257 if err := c.closePopup(ctx, id); err != nil {
258 return err
259 }
260 }
261 }
262 return nil
263}
264
265// dismissGrabs closes the grabbing popups unless the click hit one of
266// them: a click outside a menu closes it.
267func (c *Core) dismissGrabs(ctx context.Context, hit WindowID) error {
268 for _, id := range slices.Backward(slices.Clone(c.popupOrder)) {
269 p := c.popups[id]
270 if p == nil || !p.grab {
271 continue
272 }
273 // A click on a popup of the same chain keeps it; a click on
274 // anything else, its parent window included, closes it.
275 if c.popups[hit] != nil && (hit == id || c.isAncestor(id, hit) || c.isAncestor(hit, id)) {
276 continue
277 }
278 if err := c.closePopup(ctx, id); err != nil {
279 return err
280 }
281 }
282 return nil
283}
284
285// isAncestor reports whether a is a popup ancestor of b.
286func (c *Core) isAncestor(a, b WindowID) bool {
287 for p := c.popups[b]; p != nil; p = c.popups[p.parent] {
288 if p.parent == a {
289 return true
290 }
291 }
292 return false
293}
294
295// grabFocus is the topmost mapped grabbing popup, which takes the keyboard.
296func (c *Core) grabFocus() WindowID {
297 for i := len(c.popupOrder) - 1; i >= 0; i-- {
298 // A popup hidden with its parent (under a fullscreen window) has
299 // no keyboard.
300 if p := c.popups[c.popupOrder[i]]; p != nil && p.grab && p.mapped && c.visible(p.id) {
301 return p.id
302 }
303 }
304 return 0
305}
306
307// popupAt returns the topmost mapped popup under the output-local point,
308// among layer popups (overLayers) or window popups.
309func (c *Core) popupAt(sc *screen, lx, ly float64, overLayers bool) (WindowID, float64, float64) {
310 for i := len(c.popupOrder) - 1; i >= 0; i-- {
311 p := c.popups[c.popupOrder[i]]
312 if p == nil || !p.mapped || c.onLayer(c.popupRoot(p.id)) != overLayers {
313 continue
314 }
315 s, r, ok := c.windowRect(p.id)
316 if !ok || s != sc {
317 continue
318 }
319 if !overLayers && !sc.mon.frameHas(lx, ly) {
320 continue
321 }
322 if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) && c.acceptsInput(p.id, lx-float64(r.X), ly-float64(r.Y)) {
323 return p.id, lx - float64(r.X), ly - float64(r.Y)
324 }
325 }
326 return 0, 0, 0
327}
328
329// popupRoot is the window or layer a popup chain hangs from; id itself
330// when it is not a popup.
331func (c *Core) popupRoot(id WindowID) WindowID {
332 for p := c.popups[id]; p != nil; p = c.popups[id] {
333 id = p.parent
334 }
335 return id
336}
337
338// onLayer reports whether id is a mapped layer surface.
339func (c *Core) onLayer(id WindowID) bool {
340 for _, sc := range c.screens {
341 for _, l := range sc.layers {
342 if l.ID == id {
343 return true
344 }
345 }
346 }
347 return false
348}
349
350// scenePopups are the mapped popups on a screen, bottom to top.
351func (c *Core) scenePopups(sc *screen) []ports.SceneWindow {
352 var out []ports.SceneWindow
353 for _, id := range c.popupOrder {
354 p := c.popups[id]
355 if p == nil || !p.mapped {
356 continue
357 }
358 if s, r, ok := c.windowRect(id); ok && s == sc {
359 out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: c.onLayer(c.popupRoot(id))})
360 }
361 }
362 return out
363}