1package wayland
2
3import (
4 "math"
5 "slices"
6 "strings"
7
8 "github.com/bnema/go-wayland-bindings/server/wayland"
9 "github.com/bnema/go-wayland-bindings/server/xdgoutput"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/bnema/purego-libwayland/server"
12)
13
14// setOutputHDR records confirmed DRM state independently of the core layout.
15// Until the first modeset report for an output it is treated as SDR.
16func (s *Server) setOutputHDR(f ports.OutputFormats) {
17 if s.hdrOutputs == nil {
18 s.hdrOutputs = make(map[string]ports.OutputHDR)
19 }
20 old, had := s.hdrOutputs[f.Output]
21 if f.HDR == nil {
22 delete(s.hdrOutputs, f.Output)
23 } else {
24 s.hdrOutputs[f.Output] = *f.HDR
25 }
26 if (f.HDR == nil && had) || (f.HDR != nil && (!had || old != *f.HDR)) {
27 s.colorOutputChanged(f.Output)
28 }
29}
30
31// Outputs. Core sends the global layout (ports.SetOutputs); each output is
32// one wl_output global, removed when the display is unplugged. Every size
33// sent to clients (configures, xdg-output, layer configures, pointer
34// coordinates) is logical. Clients that bind wp_fractional_scale_v1 get the
35// exact scale of their output in 1/120 steps and draw at physical size
36// through wp_viewporter; others get wl_output.scale rounded up and the
37// renderer downscales.
38
39// output is one advertised display and the resources clients bound to it.
40type output struct {
41 place ports.OutputPlacement
42 global *server.Global
43 resources []*wayland.Output
44 xdgOutputs []*xdgoutput.ZxdgOutputV1
45}
46
47func (o *output) name() string { return o.place.Info.Name }
48
49// integerScale is wl_output.scale: the fractional scale rounded up.
50func integerScale(scale float64) int32 { return int32(math.Ceil(max(scale, 1) - 1e-9)) }
51
52func (o *output) description() string {
53 i := o.place.Info
54 return strings.Join(strings.Fields(i.Make+" "+i.Model+" "+i.Serial+" ("+i.Name+")"), " ")
55}
56
57// sendAll sends the full wl_output state, then done (v2+).
58func (o *output) sendAll(r *wayland.Output) {
59 i := o.place.Info
60 make, model := i.Make, i.Model
61 if make == "" {
62 make, model = "neferwl", "headless"
63 }
64 refresh := i.RefreshMilli
65 if refresh == 0 {
66 refresh = 60000
67 }
68 r.SendGeometry(int32(o.place.X), int32(o.place.Y), int32(i.PhysicalW), int32(i.PhysicalH), 0, make, model, int32(o.place.Transform))
69 // The mode stays physical; the scale tells clients how to divide it.
70 r.SendMode(3, int32(i.Width), int32(i.Height), int32(refresh))
71 if r.Version() >= 2 {
72 r.SendScale(integerScale(o.place.Scale))
73 }
74 if r.Version() >= 4 {
75 r.SendName(i.Name)
76 r.SendDescription(o.description())
77 }
78 if r.Version() >= 2 {
79 r.SendDone()
80 }
81}
82
83func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) {
84 x.SendLogicalPosition(int32(o.place.X), int32(o.place.Y))
85 x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height))
86 if withName && x.Version() >= 2 {
87 x.SendName(o.name())
88 x.SendDescription(o.description())
89 }
90}
91
92// outputOf returns the output a wl_output resource belongs to, or nil for a
93// removed one.
94func (s *Server) outputOf(r *wayland.Output) *output {
95 if r == nil {
96 return nil
97 }
98 for _, o := range s.outputs {
99 for _, v := range o.resources {
100 if v.Resource == r.Resource {
101 return o
102 }
103 }
104 }
105 return nil
106}
107
108// outputByName returns the output with that connector, else the focused one,
109// else the first; nil when there is none.
110func (s *Server) outputByName(name string) *output {
111 var first, focused *output
112 for _, o := range s.outputs {
113 if o.name() == name {
114 return o
115 }
116 if first == nil {
117 first = o
118 }
119 if o.name() == s.focusedOutput {
120 focused = o
121 }
122 }
123 if focused != nil {
124 return focused
125 }
126 return first
127}
128
129// addOutput advertises a new wl_output global.
130func (s *Server) addOutput(p ports.OutputPlacement) {
131 o := &output{place: p}
132 g, err := s.display.AddGlobal(wayland.OutputInterface, 4, func(c server.Client, v, id uint32) {
133 r, err := wayland.NewOutput(c, int32(v), id, outputHandler{})
134 if err != nil {
135 return
136 }
137 // A client may bind a global just removed: it gets the last state
138 // and no updates.
139 o.resources = append(o.resources, r)
140 r.OnDestroy = func() { o.resources = removeItem(o.resources, r) }
141 o.sendAll(r)
142 for _, m := range s.workspaceManagers {
143 if m.outputBound(o.name()) {
144 m.res.SendDone()
145 }
146 }
147 s.refreshToplevels()
148 })
149 if err != nil {
150 s.log.Warn().Err(err).Str("output", p.Info.Name).Msg("add wl_output")
151 return
152 }
153 o.global = g
154 s.outputs = append(s.outputs, o)
155 s.log.Info().Str("output", p.Info.Name).Int("x", p.X).Int("w", p.Width).Int("h", p.Height).Float64("scale", p.Scale).Msg("output added")
156}
157
158// setOutputs applies the global layout from core: new outputs are
159// advertised, gone ones withdrawn (their layer surfaces are closed), and
160// changed ones resent.
161func (s *Server) setOutputs(c ports.SetOutputs) {
162 changed := !slices.Equal(s.outputPlaces, c.Outputs)
163 s.outputPlaces = append(ports.Layout(nil), c.Outputs...)
164 defer func() {
165 if changed {
166 s.refreshOutputManagers()
167 }
168 }()
169 s.focusedOutput = c.Focused
170 want := map[string]ports.OutputPlacement{}
171 for _, p := range c.Outputs {
172 want[p.Info.Name] = p
173 }
174 kept := s.outputs[:0]
175 for _, o := range s.outputs {
176 if _, ok := want[o.name()]; ok {
177 kept = append(kept, o)
178 continue
179 }
180 o.global.Remove()
181 delete(s.hdrOutputs, o.name())
182 delete(s.colorIdentity, o.name())
183 s.outputGone(o)
184 s.log.Info().Str("output", o.name()).Msg("output removed")
185 for _, l := range s.layers {
186 if l.output == o {
187 l.close()
188 }
189 }
190 for _, l := range s.lockSurfaces {
191 if l.output == o {
192 l.unmap()
193 }
194 }
195 for _, surf := range s.surfaces {
196 surf.leave(o)
197 }
198 }
199 s.outputs = kept
200 // Windows of a removed output leave it until core moves them.
201 s.refreshToplevels()
202 for _, p := range c.Outputs {
203 o := s.outputByNameExact(p.Info.Name)
204 if o == nil {
205 s.addOutput(p)
206 continue
207 }
208 if o.place == p {
209 continue
210 }
211 scaleChanged := o.place.Scale != p.Scale
212 // A 90° to 270° change keeps the logical size: lock surfaces are
213 // logical-sized and need no reconfigure then.
214 logicalChanged := o.place.Width != p.Width || o.place.Height != p.Height
215 o.place = p
216 if logicalChanged {
217 for _, l := range s.lockSurfaces {
218 if l.output == o && !l.closed {
219 l.sendConfigure()
220 }
221 }
222 s.lockSurfaceChanged()
223 }
224 // Buffer sizes follow the mode and the scale (region and workspace
225 // sources too): sessions send new constraints when theirs changed.
226 s.refreshSessions()
227 for _, r := range o.resources {
228 o.sendAll(r)
229 }
230 for _, x := range o.xdgOutputs {
231 o.sendXDG(x, false)
232 if x.Version() < 3 {
233 x.SendDone()
234 }
235 }
236 if scaleChanged {
237 s.log.Info().Str("output", p.Info.Name).Float64("scale", p.Scale).Int("w", p.Width).Int("h", p.Height).Msg("output scale")
238 }
239 }
240 // Surfaces follow the scale of their output.
241 for _, surf := range s.surfaces {
242 surf.sendScale()
243 }
244 s.notifyColorFeedback()
245 // Layer surfaces sized from the output follow its new logical size.
246 adopted := false
247 for _, l := range s.layers {
248 if l.output == nil && !l.closed {
249 // Created before any output: it takes the focused one.
250 if l.output = s.outputByName(""); l.output != nil && l.configured {
251 l.sendConfigure()
252 adopted = adopted || l.mapped
253 continue
254 }
255 }
256 // Uses committed state only: pending requests wait for the client's commit.
257 if l.configured && (l.current.width == 0 || l.current.height == 0) {
258 l.sendConfigure()
259 }
260 }
261 if adopted {
262 s.layerChanged()
263 }
264}
265
266func (s *Server) outputByNameExact(name string) *output {
267 for _, o := range s.outputs {
268 if o.name() == name {
269 return o
270 }
271 }
272 return nil
273}
274
275type outputHandler struct{}
276
277func (outputHandler) Release(*wayland.Output) {}
278
279func registerXDGOutput(d *server.Display, s *Server) error {
280 return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) {
281 _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s})
282 })
283}
284
285type xdgOutputManager struct{ server *Server }
286
287func (xdgOutputManager) Destroy(*xdgoutput.ZxdgOutputManagerV1) {}
288func (m xdgOutputManager) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) {
289 x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{})
290 if err != nil {
291 return
292 }
293 o := m.server.outputOf(wl)
294 if o == nil {
295 // Removed output: the object stays inert.
296 return
297 }
298 o.xdgOutputs = append(o.xdgOutputs, x)
299 x.OnDestroy = func() { o.xdgOutputs = removeItem(o.xdgOutputs, x) }
300 o.sendXDG(x, true)
301 if r.Version() < 3 || wl.Version() < 2 {
302 x.SendDone()
303 } else {
304 wl.SendDone()
305 }
306}
307
308type xdgOutput struct{}
309
310func (xdgOutput) Destroy(*xdgoutput.ZxdgOutputV1) {}