1package app
2
3import (
4 "context"
5 "sync"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// outputRun drives one output until ctx ends: it renders the scenes and
11// surface contents it receives, with the cursor the client asks for.
12type outputRun func(ctx context.Context, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange) error
13
14// outputSet owns the running outputs and feeds them. Core sends one scene
15// per output; each goes to its output, latest first. Surface contents go to
16// every output, since windows move between outputs; the latest content of
17// each window is kept so a new output starts with every window drawn.
18// Only the goroutine running loop touches the set.
19type outputSet struct {
20 outs map[string]*runningOutput
21 latest map[ports.WindowID]ports.SurfaceContent
22 cursor ports.CursorChange
23 stopped chan string
24 quit chan struct{} // closed by wait: nobody reads stopped any more
25}
26
27type runningOutput struct {
28 scenes chan ports.Scene
29 contents chan ports.SurfaceContent
30 cursor chan ports.CursorChange
31 ctx context.Context
32 stop context.CancelFunc
33 err error
34 done chan struct{}
35}
36
37func newOutputSet() *outputSet {
38 return &outputSet{outs: map[string]*runningOutput{}, latest: map[ports.WindowID]ports.SurfaceContent{}, stopped: make(chan string), quit: make(chan struct{})}
39}
40
41// start runs an output in its own goroutine and replays the window contents.
42func (s *outputSet) start(ctx context.Context, name string, run outputRun) {
43 octx, stop := context.WithCancel(ctx)
44 r := &runningOutput{scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 64), cursor: make(chan ports.CursorChange, 1), ctx: octx, stop: stop, done: make(chan struct{})}
45 s.outs[name] = r
46 go func() {
47 r.err = run(octx, r.scenes, r.contents, r.cursor)
48 close(r.done)
49 select {
50 case s.stopped <- name:
51 case <-s.quit:
52 }
53 }()
54 r.cursor <- s.cursor
55 for _, c := range s.latest {
56 r.send(c)
57 }
58}
59
60// setCursor gives every output the latest cursor, replacing an unread one.
61func (s *outputSet) setCursor(c ports.CursorChange) {
62 s.cursor = c
63 for _, r := range s.outs {
64 select {
65 case <-r.cursor:
66 default:
67 }
68 r.cursor <- c
69 }
70}
71
72// send hands content to the output unless it has stopped.
73func (r *runningOutput) send(c ports.SurfaceContent) {
74 select {
75 case r.contents <- c:
76 case <-r.ctx.Done():
77 case <-r.done:
78 }
79}
80
81// scenes routes each scene to its output, replacing an unread one.
82func (s *outputSet) scenes(set []ports.Scene) {
83 for _, sc := range set {
84 if r := s.outs[sc.Output]; r != nil {
85 select {
86 case <-r.scenes:
87 default:
88 }
89 r.scenes <- sc
90 }
91 }
92}
93
94func (s *outputSet) content(c ports.SurfaceContent) {
95 if c.Empty() {
96 delete(s.latest, c.ID)
97 } else {
98 s.latest[c.ID] = c
99 }
100 for _, r := range s.outs {
101 r.send(c)
102 }
103}
104
105// finish forgets a stopped output and returns its error.
106func (s *outputSet) finish(name string) error {
107 r := s.outs[name]
108 if r == nil {
109 return nil
110 }
111 <-r.done
112 r.stop()
113 delete(s.outs, name)
114 return r.err
115}
116
117// wait stops every output and returns the first error.
118func (s *outputSet) wait() error {
119 var first error
120 close(s.quit)
121 for name, r := range s.outs {
122 r.stop()
123 <-r.done
124 if first == nil && r.err != nil {
125 first = r.err
126 }
127 delete(s.outs, name)
128 }
129 return first
130}
131
132// cursors routes pointer moves from the input goroutine to the cursor of
133// the output under the pointer and hides the others.
134type cursors struct {
135 mu sync.Mutex
136 all map[string]cursor
137 last string
138}
139
140// cursor is a hardware or software cursor of one output.
141type cursor interface {
142 Move(x, y float64)
143 Hide()
144}
145
146func newCursors() *cursors { return &cursors{all: map[string]cursor{}} }
147
148// set registers the cursor of an output; nil removes it.
149func (c *cursors) set(output string, cur cursor) {
150 c.mu.Lock()
151 defer c.mu.Unlock()
152 if cur == nil {
153 delete(c.all, output)
154 return
155 }
156 c.all[output] = cur
157 // Only the output under the pointer shows it; before the first move,
158 // none does (a new cursor would show at its top-left corner).
159 if output != c.last {
160 cur.Hide()
161 }
162}
163
164// move places the cursor on output at physical (x, y).
165func (c *cursors) move(output string, x, y float64) {
166 c.mu.Lock()
167 defer c.mu.Unlock()
168 if output != c.last {
169 for name, cur := range c.all {
170 if name != output {
171 cur.Hide()
172 }
173 }
174 c.last = output
175 }
176 if cur := c.all[output]; cur != nil {
177 cur.Move(x, y)
178 }
179}