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