1package ui
2
3import (
4 "cmp"
5 "context"
6 "errors"
7 "fmt"
8 "os"
9 "slices"
10 "time"
11
12 "github.com/bnema/nefergui/internal/css"
13 "github.com/bnema/nefergui/internal/platform/wayland"
14 "github.com/bnema/nefergui/internal/presentation/session"
15 "github.com/bnema/nefergui/internal/render"
16 "github.com/bnema/nefergui/internal/text"
17)
18
19// outputSurface is one layer surface and its own layout runtime: outputs
20// differ in size and scale, so each lays the shared view out separately.
21type outputSurface struct {
22 w *wayland.Window
23 output uint32
24 r *runtime
25 s *session.Session
26 p *session.Pump
27}
28
29// outputSet runs one layer surface per output on a single connection and
30// owner loop, following the session-lock structure. Outputs added later gain a
31// surface; removed outputs, or surfaces the compositor closes, lose theirs.
32type outputSet struct {
33 ctx context.Context
34 conn *wayland.Connection
35 opts wayland.SurfaceOptions
36 width int32
37 height int32
38 transparent bool
39 sheet css.Sheet
40 catalog *text.Catalog
41 view func(*Frame)
42 hooks *inputHooks
43 surfaces map[*wayland.Window]*outputSurface
44 pending map[*wayland.Window]uint32 // created, waiting for configure and feedback
45 covered map[uint32]bool // outputs with a surface or a pending one
46 gpuWake chan struct{}
47}
48
49// runAllOutputs is Run with LayerConfig.AllOutputs. It returns on cancellation
50// or the first failure; it never ends because outputs come and go.
51func runAllOutputs(ctx context.Context, cfg windowConfig, view func(*Frame)) (err error) {
52 if cfg.onSurface != nil || cfg.onResize != nil {
53 return errors.New("nefergui: AllOutputs excludes OnSurface and OnResize")
54 }
55 opts, err := cfg.layer.surfaceOptions()
56 if err != nil {
57 return err
58 }
59 a := &outputSet{ctx: ctx, opts: opts, width: int32(cfg.width), height: int32(cfg.height), transparent: cfg.transparent, view: view,
60 hooks: &inputHooks{onInput: cfg.onInput}, surfaces: map[*wayland.Window]*outputSurface{}, pending: map[*wayland.Window]uint32{},
61 covered: map[uint32]bool{}, gpuWake: make(chan struct{}, 1)}
62 if cfg.styles != "" {
63 data, e := os.ReadFile(cfg.styles)
64 if e != nil {
65 return fmt.Errorf("nefergui: styles: %w", e)
66 }
67 a.sheet = css.Parse(string(data))
68 }
69 if a.catalog, err = text.Load(text.SystemSource{}); err != nil {
70 return fmt.Errorf("nefergui: fonts: %w", err)
71 }
72 if len(a.catalog.Faces) == 0 {
73 return errors.New("nefergui: no usable fonts")
74 }
75 if a.conn, err = wayland.ConnectConnection(ctx, ""); err != nil {
76 return fmt.Errorf("nefergui: open platform: %w", err)
77 }
78 defer func() {
79 a.closeAll()
80 err = errors.Join(err, a.conn.Close())
81 }()
82 // Surfaces for the outputs present now are configured before the reader
83 // starts; output events seen meanwhile are held and handled right after.
84 if err = a.reconcile(); err != nil {
85 return err
86 }
87 events := a.conn.StartReader()
88 for _, ev := range a.conn.TakeHeld() {
89 if err = a.handle(ev); err != nil {
90 return err
91 }
92 }
93 ext := make(chan struct{}, 1)
94 if cfg.wake != nil {
95 loopCtx, stop := context.WithCancel(ctx)
96 defer stop()
97 go forward(loopCtx, cfg.wake, ext)
98 }
99 return a.loop(events, ext)
100}
101
102// forward relays external wake tokens to the owner loop until ctx ends or ch
103// closes; tokens coalesce.
104func forward(ctx context.Context, ch <-chan struct{}, out chan<- struct{}) {
105 for {
106 select {
107 case <-ctx.Done():
108 return
109 case _, ok := <-ch:
110 if !ok {
111 return
112 }
113 select {
114 case out <- struct{}{}:
115 default:
116 }
117 }
118 }
119}
120
121func (a *outputSet) loop(events <-chan wayland.Event, ext <-chan struct{}) error {
122 for {
123 if err := a.promote(); err != nil {
124 return err
125 }
126 // Every Redraw runs on this goroutine, so draining each runtime's own
127 // wake here sees every request, including Build follow-ups.
128 a.drainWakes()
129 retry := false
130 for _, o := range a.surfaces {
131 again, err := o.p.Step()
132 if err != nil {
133 return err
134 }
135 retry = retry || again
136 }
137 if a.drainWakes() {
138 continue // a Build asked for a follow-up frame
139 }
140 var retryC <-chan time.Time
141 if retry {
142 retryC = time.After(5 * time.Millisecond)
143 }
144 select {
145 case <-a.ctx.Done():
146 return a.ctx.Err()
147 case <-ext:
148 for _, o := range a.surfaces {
149 o.r.Redraw()
150 }
151 case <-a.gpuWake:
152 case <-retryC:
153 case ev := <-events:
154 if err := a.handle(ev); err != nil {
155 return err
156 }
157 }
158 }
159}
160
161// drainWakes invalidates every surface whose runtime requested a frame and
162// reports whether any did.
163func (a *outputSet) drainWakes() bool {
164 woke := false
165 for _, o := range a.surfaces {
166 select {
167 case <-o.r.wake:
168 o.p.Invalidate()
169 woke = true
170 default:
171 }
172 }
173 return woke
174}
175
176func (a *outputSet) handle(ev wayland.Event) error {
177 switch ev.Kind {
178 case wayland.BufferRelease:
179 if o := a.surfaces[ev.Window]; o != nil {
180 return o.p.BufferReleased(ev.BufferID)
181 }
182 return nil
183 case wayland.OutputRemoved:
184 a.removeOutput(ev.Output.Global)
185 }
186 if err := a.conn.Apply(ev); err != nil {
187 return err
188 }
189 if w := ev.Window; w != nil && w.Closed {
190 // The compositor closed this surface (layer_surface.closed); the output
191 // stays covered so it is not recreated in a loop.
192 a.closeWindow(w)
193 return nil
194 }
195 switch ev.Kind {
196 case wayland.OutputGlobal, wayland.OutputNamed, wayland.OutputAdded:
197 return a.reconcile()
198 }
199 // Seat changes queue input (leave, focus-out) on every window.
200 for _, o := range a.surfaces {
201 if err := a.input(o); err != nil {
202 return err
203 }
204 }
205 return nil
206}
207
208// reconcile creates a layer surface for every named output without one.
209func (a *outputSet) reconcile() error {
210 for _, out := range a.conn.Outputs() {
211 if a.covered[out.Global] {
212 continue
213 }
214 a.covered[out.Global] = true
215 l := *a.opts.Layer
216 l.OutputGlobal = out.Global
217 opts := a.opts
218 opts.Layer = &l
219 w, err := a.conn.NewWindow(a.ctx, a.width, a.height, a.transparent, opts)
220 if err != nil {
221 return fmt.Errorf("nefergui: layer surface for output %s: %w", out.Name, err)
222 }
223 a.pending[w] = out.Global
224 }
225 return nil
226}
227
228// promote opens the render path of every configured window with complete
229// dmabuf feedback, in ascending output order.
230func (a *outputSet) promote() error {
231 ready := make([]*wayland.Window, 0, len(a.pending))
232 for w := range a.pending {
233 if w.Configured && w.FeedbackDone {
234 ready = append(ready, w)
235 }
236 }
237 slices.SortFunc(ready, func(x, y *wayland.Window) int { return cmp.Compare(a.pending[x], a.pending[y]) })
238 for _, w := range ready {
239 output := a.pending[w]
240 delete(a.pending, w)
241 if w.Closed { // closed by the compositor before the reader started
242 _ = w.Close()
243 continue
244 }
245 o, err := a.open(w, output) // closes w on failure
246 if err != nil {
247 return fmt.Errorf("nefergui: layer render path: %w", err)
248 }
249 a.surfaces[w] = o
250 }
251 return nil
252}
253
254func (a *outputSet) open(w *wayland.Window, output uint32) (*outputSurface, error) {
255 s, err := session.OpenWindow(w, a.transparent)
256 if err != nil {
257 return nil, err
258 }
259 r := newRuntime()
260 r.styles = css.Compile(css.UA(), a.sheet)
261 r.setTextEngine(text.NewEngine(a.catalog))
262 o := &outputSurface{w: w, output: output, r: r, s: s}
263 if o.p, err = s.NewPump(a.ctx, a.gpuWake, func() (render.Frame, bool, error) { return a.draw(o) }); err != nil {
264 _ = s.Close()
265 return nil, err
266 }
267 return o, nil
268}
269
270func (a *outputSet) draw(o *outputSurface) (render.Frame, bool, error) {
271 w := o.w
272 o.r.route(platformInput{Kind: "resize", Width: float64(w.Width), Height: float64(w.Height), Scale: w.Scale})
273 if !o.r.Build(a.view) {
274 return render.Frame{}, false, nil
275 }
276 if o.r.consumedInput() {
277 // Control events may have changed the shared model: the other outputs
278 // rebuild without events, so this cannot bounce back.
279 for _, other := range a.surfaces {
280 if other != o {
281 other.r.Redraw()
282 }
283 }
284 }
285 if err := o.r.applyInputRects(w.StageInputRects); err != nil {
286 return render.Frame{}, false, err
287 }
288 if err := w.SetCursor(cursorShape(o.r.cursor())); err != nil {
289 return render.Frame{}, false, err
290 }
291 pw, ph, err := w.PhysicalSize()
292 if err != nil {
293 return render.Frame{}, false, err
294 }
295 frame, err := o.s.Preparer.Prepare(o.r.output.Display, w.Scale, int(pw), int(ph))
296 return frame, true, err
297}
298
299// input routes pointer input to the surface's runtime. A raw hook that
300// changed the model redraws every output, since they share it.
301func (a *outputSet) input(o *outputSurface) error {
302 inputs := o.w.DrainInput(nil)
303 if len(inputs) == 0 {
304 return nil
305 }
306 for _, in := range inputs {
307 switch {
308 case in.Enter:
309 o.w.PointerEntered(in.Serial, in.PointerGen)
310 case in.Kind == "leave":
311 o.w.PointerLeft()
312 }
313 if a.hooks.input(in) {
314 for _, other := range a.surfaces {
315 other.r.Redraw()
316 }
317 }
318 o.r.routeInput(in)
319 }
320 o.p.Invalidate()
321 return o.w.SetCursor(cursorShape(o.r.cursor()))
322}
323
324func (a *outputSet) removeOutput(global uint32) {
325 delete(a.covered, global)
326 for w, output := range a.pending {
327 if output == global {
328 delete(a.pending, w)
329 _ = w.Close()
330 }
331 }
332 for w, o := range a.surfaces {
333 if o.output == global {
334 delete(a.surfaces, w)
335 o.close()
336 }
337 }
338}
339
340func (a *outputSet) closeWindow(w *wayland.Window) {
341 if _, ok := a.pending[w]; ok {
342 delete(a.pending, w)
343 _ = w.Close()
344 }
345 if o := a.surfaces[w]; o != nil {
346 delete(a.surfaces, w)
347 o.close()
348 }
349}
350
351func (o *outputSurface) close() {
352 o.p.Close()
353 _ = o.s.Close()
354}
355
356func (a *outputSet) closeAll() {
357 for w, o := range a.surfaces {
358 delete(a.surfaces, w)
359 o.close()
360 }
361 for w := range a.pending {
362 delete(a.pending, w)
363 _ = w.Close()
364 }
365}