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