1//go:build linux
2
3package wayland
4
5import (
6 "errors"
7 "fmt"
8 "math"
9 "slices"
10 "sort"
11 "strings"
12
13 "github.com/bnema/wlturbo"
14 "github.com/bnema/wlturbo/protocol/core"
15 "github.com/bnema/wlturbo/wl"
16
17 "github.com/bnema/nefergui/internal/platform/wayland/layershell"
18)
19
20// zwlr_layer_shell_v1 layers. The values are the wire values.
21const (
22 LayerBackground uint32 = 0
23 LayerBottom uint32 = 1
24 LayerTop uint32 = 2
25 LayerOverlay uint32 = 3
26)
27
28// zwlr_layer_surface_v1 anchor bits. They may be OR-ed together.
29const (
30 AnchorTop uint32 = 1
31 AnchorBottom uint32 = 2
32 AnchorLeft uint32 = 4
33 AnchorRight uint32 = 8
34
35 anchorAll = AnchorTop | AnchorBottom | AnchorLeft | AnchorRight
36)
37
38// zwlr_layer_surface_v1 keyboard interactivity modes.
39const (
40 KeyboardNone uint32 = 0
41 KeyboardExclusive uint32 = 1
42 // KeyboardOnDemand needs zwlr_layer_shell_v1 version 4.
43 KeyboardOnDemand uint32 = 2
44)
45
46const (
47 layerShellSupported = 4
48 outputNameVersion = 4 // wl_output.name was added in version 4
49 // MaxLayerDimension bounds a compositor-assigned layer surface extent.
50 MaxLayerDimension = 16384
51 // DefaultLayerNamespace is used when LayerOptions.Namespace is empty. The
52 // namespace is an opaque, generic role hint; nothing privileged depends on it.
53 DefaultLayerNamespace = "nefergui"
54)
55
56// Rect is an integer rectangle in surface-local logical coordinates.
57type Rect struct{ X, Y, W, H int32 }
58
59// LayerOptions selects wlr-layer-shell instead of xdg-toplevel.
60type LayerOptions struct {
61 // Output is the exact wl_output name (for example "DP-1"). Empty lets the
62 // compositor choose its preferred output. A named output that is absent
63 // fails the connection; a named output removed later closes the window.
64 Output string
65 // OutputGlobal, when non-zero, selects an output lifetime returned by
66 // Connection.Outputs instead of Output. The connection owner handles its
67 // removal (OutputRemoved); the window is not closed for it.
68 OutputGlobal uint32
69 // Namespace is the layer surface namespace; empty selects DefaultLayerNamespace.
70 Namespace string
71 // Anchor is a bit set of Anchor* values.
72 Anchor uint32
73 // Layer is one of the Layer* values.
74 Layer uint32
75 // Keyboard is one of the Keyboard* values.
76 Keyboard uint32
77 // ExclusiveZone follows zwlr_layer_surface_v1.set_exclusive_zone
78 // (positive reserves, 0 is neutral, -1 ignores other zones).
79 ExclusiveZone int32
80 // Margin is ordered top, right, bottom, left, like the protocol request.
81 Margin [4]int32
82}
83
84// SurfaceOptions configures the role and input region of a new Window. The zero
85// value selects the default xdg-toplevel with full-surface input.
86type SurfaceOptions struct {
87 // Layer, when non-nil, creates a layer surface and binds no xdg shell.
88 Layer *LayerOptions
89 // Lock selects a private output lifetime on this Connection's sole lock.
90 // It is incompatible with Layer, transparency and custom input regions.
91 Lock *LockOptions
92 // InputRects is the initial input region in surface-local logical pixels.
93 // nil means the whole surface (the protocol default); a non-nil empty slice
94 // means no input (pointer and touch pass through).
95 InputRects []Rect
96}
97
98// Validate reports the first invalid option.
99func (o SurfaceOptions) Validate() error {
100 if o.Lock != nil && (o.Layer != nil || o.InputRects != nil) {
101 return errors.New("lock role cannot have layer or custom input")
102 }
103 if o.Layer != nil {
104 if err := o.Layer.Validate(); err != nil {
105 return err
106 }
107 }
108 return validateRects(o.InputRects)
109}
110
111// Validate reports the first invalid layer option.
112func (o LayerOptions) Validate() error {
113 switch {
114 case o.Layer > LayerOverlay:
115 return fmt.Errorf("invalid layer %d", o.Layer)
116 case o.Anchor&^anchorAll != 0:
117 return fmt.Errorf("invalid anchor bits %#x", o.Anchor)
118 case o.ExclusiveZone < -1:
119 return fmt.Errorf("invalid exclusive zone %d", o.ExclusiveZone)
120 case o.Keyboard > KeyboardOnDemand:
121 return fmt.Errorf("invalid keyboard interactivity %d", o.Keyboard)
122 case strings.ContainsRune(o.Namespace, 0):
123 return errors.New("layer namespace contains NUL")
124 case strings.ContainsRune(o.Output, 0):
125 return errors.New("layer output contains NUL")
126 case o.OutputGlobal != 0 && o.Output != "":
127 return errors.New("layer output name and global are exclusive")
128 }
129 return nil
130}
131
132func validateRects(rects []Rect) error {
133 for i, r := range rects {
134 if r.W <= 0 || r.H <= 0 {
135 return fmt.Errorf("input rect %d has non-positive size %dx%d", i, r.W, r.H)
136 }
137 if int64(r.X)+int64(r.W) > math.MaxInt32 || int64(r.Y)+int64(r.H) > math.MaxInt32 {
138 return fmt.Errorf("input rect %d overflows", i)
139 }
140 }
141 return nil
142}
143
144// layerConfigureSize validates a configure extent. Zero leaves that axis unchanged.
145func layerConfigureSize(width, height uint32) (int32, int32, error) {
146 if width > MaxLayerDimension || height > MaxLayerDimension {
147 return 0, 0, fmt.Errorf("layer configure size %dx%d exceeds %d", width, height, MaxLayerDimension)
148 }
149 return int32(width), int32(height), nil
150}
151
152// SetTitle sets the toplevel title. Layer surfaces have no title, so it is a no-op.
153func (w *Window) SetTitle(title string) error {
154 if w == nil || w.Toplevel == nil {
155 return nil
156 }
157 return w.Toplevel.SetTitle(title)
158}
159
160// IsLayer reports whether the window is a layer surface.
161func (w *Window) IsLayer() bool { return w != nil && w.LayerSurface != nil }
162
163// StageInputRects validates rects and stages them as the pending input region
164// without committing; the next commit (normally Present) applies them. nil
165// restores the full surface and a non-nil empty slice makes the surface pass
166// all input through. A request identical to the current one sends nothing and
167// allocates nothing. Call from the session owner loop only.
168func (w *Window) StageInputRects(rects []Rect) error {
169 _, err := w.stageInputRects(rects)
170 return err
171}
172
173// SetInputRects stages rects like StageInputRects and, only if they changed and
174// the window is configured, commits immediately. Before the first configure
175// the initial commit carries the region.
176func (w *Window) SetInputRects(rects []Rect) error {
177 changed, err := w.stageInputRects(rects)
178 if err != nil || !changed || !w.Configured {
179 return err
180 }
181 return w.Surface.Commit()
182}
183
184func (w *Window) stageInputRects(rects []Rect) (changed bool, err error) {
185 if w == nil || w.Surface == nil || w.Compositor == nil {
186 return false, errors.New("window has no surface")
187 }
188 if err = validateRects(rects); err != nil {
189 return false, err
190 }
191 if (rects != nil) == w.inputCustom && slices.Equal(rects, w.inputRects) {
192 return false, nil
193 }
194 if err = w.stageInputRegion(rects); err != nil {
195 return false, err
196 }
197 w.inputCustom = rects != nil
198 w.inputRects = append(w.inputRects[:0], rects...)
199 return true, nil
200}
201
202func (w *Window) stageInputRegion(rects []Rect) error {
203 if rects == nil {
204 return w.Surface.SetInputRegion(nil)
205 }
206 region, err := w.Compositor.CreateRegion()
207 if err != nil {
208 return err
209 }
210 for _, r := range rects {
211 if err = region.Add(r.X, r.Y, r.W, r.H); err != nil {
212 break
213 }
214 }
215 if err == nil {
216 err = w.Surface.SetInputRegion(region)
217 }
218 _ = region.Destroy()
219 return err
220}
221
222// applyLayerSize applies a validated layer configure; zero axes are unchanged.
223func (w *Window) applyLayerSize(width, height int32) {
224 if width > 0 && w.Width != width {
225 w.Width, w.Dirty = width, true
226 }
227 if height > 0 && w.Height != height {
228 w.Height, w.Dirty = height, true
229 }
230}
231
232// bindLayerShell binds zwlr_layer_shell_v1 (up to version 4).
233func (w *Window) bindLayerShell(bind func(string, uint32, uint32, wl.Proxy) error) error {
234 ls := layershell.NewLayerShell(w.Display.Context())
235 if err := bind(layershell.LayerShellInterface, layerShellSupported, 1, ls); err != nil {
236 w.Display.Context().Unregister(ls) // never bound: no destructor to send
237 return err
238 }
239 w.LayerShell = ls
240 return nil
241}
242
243type outputCandidate struct {
244 out *core.Output
245 global uint32
246 name string
247}
248
249// releaseOutputs releases every candidate except keep.
250func releaseOutputs(cands []*outputCandidate, keep *outputCandidate) {
251 for _, c := range cands {
252 if c != keep {
253 _ = c.out.Release()
254 }
255 }
256}
257
258// selectOutput binds every wl_output that can report a name and returns the
259// global name of the exact match. Unmatched outputs are released.
260func (w *Window) selectOutput(name string) (*core.Output, uint32, error) {
261 if w.connection != nil {
262 for _, out := range w.connection.outputs {
263 if out.Name == name {
264 return out.proxy, out.Global, nil
265 }
266 }
267 return nil, 0, &CapabilityError{Name: core.OutputInterface, Cause: fmt.Errorf("output %q not found in applied inventory", name)}
268 }
269 reg := w.Display.Registry()
270 var globals []wl.Global
271 legacy := 0 // outputs older than v4 cannot report a name
272 for _, g := range reg.GetGlobals() {
273 if g.Interface != core.OutputInterface {
274 continue
275 }
276 if g.Version >= outputNameVersion {
277 globals = append(globals, g)
278 } else {
279 legacy++
280 }
281 }
282 sort.Slice(globals, func(i, j int) bool { return globals[i].Name < globals[j].Name })
283 cands := make([]*outputCandidate, 0, len(globals))
284 ctx := w.Display.Context()
285 for _, g := range globals {
286 c := &outputCandidate{out: core.NewOutput(ctx), global: g.Name}
287 c.out.OnName(func(n string) { c.name = n })
288 if err := reg.Bind(g.Name, core.OutputInterface, outputNameVersion, c.out); err != nil {
289 ctx.Unregister(c.out)
290 releaseOutputs(cands, nil)
291 return nil, 0, &CapabilityError{Name: core.OutputInterface, Cause: err}
292 }
293 cands = append(cands, c)
294 }
295 if len(cands) > 0 {
296 if err := w.Display.Roundtrip(); err != nil {
297 releaseOutputs(cands, nil)
298 return nil, 0, err
299 }
300 }
301 var chosen *outputCandidate
302 for _, c := range cands {
303 if c.name == name {
304 chosen = c
305 break
306 }
307 }
308 releaseOutputs(cands, chosen)
309 if chosen == nil {
310 cause := fmt.Errorf("output %q not found", name)
311 if legacy > 0 {
312 cause = fmt.Errorf("output %q not found; %d wl_output global(s) older than version %d cannot report names", name, legacy, outputNameVersion)
313 }
314 return nil, 0, &CapabilityError{Name: core.OutputInterface, Cause: cause}
315 }
316 return chosen.out, chosen.global, nil
317}
318
319// createLayerSurface creates the role object and stages all double-buffered
320// state. The caller performs the initial empty commit.
321func (w *Window) createLayerSurface(o LayerOptions) error {
322 if o.Keyboard == KeyboardOnDemand && w.LayerShell.Version() < 4 {
323 return &CapabilityError{Name: layershell.LayerShellInterface, Cause: fmt.Errorf("on-demand keyboard requires version 4, compositor negotiated %d", w.LayerShell.Version())}
324 }
325 var out *core.Output
326 if o.OutputGlobal != 0 {
327 if w.connection == nil {
328 return errors.New("layer output global requires a connection")
329 }
330 sel, ok := w.connection.outputs[o.OutputGlobal]
331 if !ok || sel.proxy == nil {
332 return fmt.Errorf("layer output %d unavailable", o.OutputGlobal)
333 }
334 out = sel.proxy
335 } else if o.Output != "" {
336 var global uint32
337 var err error
338 if out, global, err = w.selectOutput(o.Output); err != nil {
339 return err
340 }
341 w.layerOutputGlobal, w.layerOutputSelected = global, true
342 w.Display.Registry().AddGlobalRemoveHandler(outputRemoved{w})
343 }
344 ns := o.Namespace
345 if ns == "" {
346 ns = DefaultLayerNamespace
347 }
348 ls, err := w.LayerShell.GetLayerSurface(w.Surface, out, o.Layer, ns)
349 if out != nil && w.connection == nil {
350 // The compositor keeps its own reference to the output. A shared
351 // connection owns the proxy in its output inventory instead.
352 _ = out.Release()
353 }
354 if err != nil {
355 return err
356 }
357 w.LayerSurface = ls
358 ls.OnClosed(func() { w.postClosed() })
359 ls.OnConfigure(func(serial, width, height uint32) {
360 cw, ch, e := layerConfigureSize(width, height)
361 if e != nil {
362 w.feedbackFailure(e)
363 return
364 }
365 if w.readerStarted.Load() {
366 w.post(Event{Kind: ConfigureLayer, Width: cw, Height: ch, Serial: serial})
367 return
368 }
369 w.applyLayerSize(cw, ch)
370 if e = ls.AckConfigure(serial); e != nil {
371 w.feedbackFailure(e)
372 return
373 }
374 w.Configured, w.FrameReady = true, true
375 })
376 // Opposite anchors let the compositor assign that axis (size 0).
377 reqW, reqH := uint32(w.Width), uint32(w.Height)
378 if o.Anchor&(AnchorLeft|AnchorRight) == AnchorLeft|AnchorRight {
379 reqW = 0
380 }
381 if o.Anchor&(AnchorTop|AnchorBottom) == AnchorTop|AnchorBottom {
382 reqH = 0
383 }
384 if err = ls.SetSize(reqW, reqH); err != nil {
385 return err
386 }
387 if err = ls.SetAnchor(o.Anchor); err != nil {
388 return err
389 }
390 if err = ls.SetExclusiveZone(o.ExclusiveZone); err != nil {
391 return err
392 }
393 if err = ls.SetMargin(o.Margin[0], o.Margin[1], o.Margin[2], o.Margin[3]); err != nil {
394 return err
395 }
396 return ls.SetKeyboardInteractivity(o.Keyboard)
397}
398
399// outputRemoved closes the window when its selected output disappears.
400type outputRemoved struct{ w *Window }
401
402func (h outputRemoved) HandleRegistryGlobalRemove(ev wlturbo.RegistryGlobalRemoveEvent) {
403 if h.w.layerOutputSelected && ev.Name == h.w.layerOutputGlobal {
404 h.w.postClosed()
405 }
406}
407
408func (w *Window) postClosed() {
409 if w.readerStarted.Load() {
410 w.post(Event{Kind: CloseEvent})
411 return
412 }
413 w.Closed = true
414}
415
416// closeLayer destroys the layer role and its factory; the wl_surface goes last.
417func (w *Window) closeLayer() {
418 if w.LayerSurface != nil {
419 _ = w.LayerSurface.Destroy()
420 w.LayerSurface = nil
421 }
422 if w.LayerShell != nil {
423 // zwlr_layer_shell_v1.destroy needs version 3; otherwise forget it locally.
424 if w.LayerShell.Destroy() != nil {
425 if c := w.LayerShell.Context(); c != nil {
426 c.Unregister(w.LayerShell)
427 }
428 }
429 w.LayerShell = nil
430 }
431}