1//go:build linux
2
3package wayland
4
5import (
6 "fmt"
7 "os"
8 "time"
9
10 "github.com/bnema/nefergui/internal/keyboard"
11 "github.com/bnema/wlturbo/protocol/core"
12 "github.com/bnema/wlturbo/wl"
13)
14
15// InputReset tells the runtime to synthesize pointer leave, release all buttons,
16// keyboard focus-out and repeat cancellation before processing later inputs.
17const InputReset = "reset"
18
19// Input is the platform-to-UI port. wl_surface local fixed coordinates are
20// already logical pixels and must not be divided by buffer scale.
21type Input struct {
22 Kind string // motion, press, release, axis, leave, key, focus-in, focus-out, reset
23 X, Y, DX, DY float64
24 Button, Serial, Axis, AxisSource uint32
25 AxisStop bool
26 Enter bool // motion from wl_pointer.enter; Serial is the enter serial
27 PointerGen uint64 // wl_pointer generation that sent Enter
28 Value120 int32
29 Key keyboard.Key
30}
31
32// InputAdapter is owned by the session loop. Never call ApplyInput on the reader.
33// The session loop also owns the repeat timer (Tick and NextRepeat).
34type InputAdapter struct {
35 Keyboard *keyboard.Keyboard
36 serial uint32
37 focused bool
38}
39
40// resetHeld drops held keys, repeat and compose state after InputReset while
41// keeping keyboard focus, so later key events still apply without a new enter.
42func (a *InputAdapter) resetHeld() {
43 if a != nil && a.Keyboard != nil {
44 a.Keyboard.Focus(a.focused)
45 }
46}
47
48func NewInputAdapter(locale string) (*InputAdapter, error) {
49 k, err := keyboard.New(locale)
50 if err != nil {
51 return nil, err
52 }
53 k.Focus(false)
54 return &InputAdapter{Keyboard: k}, nil
55}
56func (a *InputAdapter) Close() {
57 if a != nil && a.Keyboard != nil {
58 a.Keyboard.Close()
59 }
60}
61func (a *InputAdapter) SelectionSerial() uint32 { return a.serial }
62func (a *InputAdapter) Tick(now time.Time) (Input, bool) {
63 key, ok := a.Keyboard.Tick(now)
64 return Input{Kind: "key", Key: key}, ok
65}
66func (a *InputAdapter) NextRepeat() (time.Time, bool) { return a.Keyboard.NextRepeat() }
67
68// InputAdapter returns the loop-owned interpreter; the reader never calls it.
69func (w *Window) InputAdapter() *InputAdapter { return w.inputAdapter }
70
71// ApplyInput consumes the raw event including its keymap FD, on every path.
72func (a *InputAdapter) ApplyInput(ev Event, now time.Time) ([]Input, error) {
73 switch ev.Kind {
74 case InputFrame:
75 return ev.Inputs, nil
76 case InputKeymap:
77 if ev.FD < 0 {
78 return nil, fmt.Errorf("missing keymap fd")
79 }
80 if ev.Format != 1 || ev.Size == 0 || ev.Size > 16<<20 {
81 CloseEventFD(ev)
82 return nil, fmt.Errorf("unsupported keymap format or size")
83 }
84 return nil, a.Keyboard.ReplaceFD(ev.FD, int(ev.Size))
85 case InputFocusIn:
86 a.Keyboard.Focus(true)
87 a.focused = true
88 a.serial = ev.Serial
89 return []Input{{Kind: "focus-in", Serial: ev.Serial}}, nil
90 case InputFocusOut:
91 a.Keyboard.Focus(false)
92 a.focused = false
93 a.serial = 0
94 return []Input{{Kind: "focus-out"}}, nil
95 case InputKey:
96 if ev.Pressed && a.Keyboard != nil {
97 a.serial = ev.Serial
98 }
99 key, err := a.Keyboard.Event(ev.Code, ev.Pressed, now)
100 if err != nil || key.Physical == 0 {
101 return nil, err
102 }
103 return []Input{{Kind: "key", Key: key, Serial: ev.Serial}}, nil
104 case InputModifiers:
105 return nil, a.Keyboard.Mask(ev.Mods[0], ev.Mods[1], ev.Mods[2], 0, 0, ev.Mods[3])
106 case InputRepeatInfo:
107 a.Keyboard.RepeatInfo(int(ev.Rate), time.Duration(ev.Delay)*time.Millisecond)
108 return nil, nil
109 }
110 return nil, fmt.Errorf("not an input event: %d", ev.Kind)
111}
112
113// CloseEventFD releases a keymap event not delivered to the loop, including FD 0.
114func CloseEventFD(ev Event) {
115 if ev.Kind == InputKeymap && ev.FD >= 0 {
116 _ = os.NewFile(uintptr(ev.FD), "keymap").Close()
117 }
118}
119
120// BindSeat binds the optional initial seat. The reader posts capability changes
121// and global removal; ApplySeat runs exclusively on the session loop.
122func (w *Window) BindSeat() error {
123 seat := core.NewSeat(w.Display.Context())
124 g, ok := w.Display.Registry().FindGlobal(core.SeatInterface)
125 if !ok {
126 w.Display.Context().Unregister(seat)
127 return nil
128 }
129 version, err := w.Display.Registry().BindNegotiated(core.SeatInterface, 9, seat)
130 if err != nil {
131 w.Display.Context().Unregister(seat)
132 return err
133 }
134 w.Seat, w.seatName, w.seatVersion = seat, g.Name, version
135 seat.OnCapabilities(func(c uint32) { w.post(Event{Kind: SeatCapabilities, Capabilities: c}) })
136 w.Display.Registry().AddGlobalRemoveHandler(seatRemoved{w: w, name: g.Name})
137 return nil
138}
139
140type seatRemoved struct {
141 w *Window
142 name uint32
143}
144
145func (r seatRemoved) HandleRegistryGlobalRemove(e wl.RegistryGlobalRemoveEvent) {
146 if e.Name == r.name {
147 r.w.post(Event{Kind: SeatRemoved})
148 }
149}
150func (w *Window) ApplySeat(ev Event) error {
151 switch ev.Kind {
152 case SeatRemoved:
153 if w.WLKeyboard != nil {
154 w.post(Event{Kind: InputFocusOut})
155 }
156 return w.releaseSeat()
157 case SeatCapabilities:
158 if w.Seat == nil {
159 return nil
160 }
161 if ev.Capabilities&1 == 0 && w.Pointer != nil {
162 w.dropCursorDevice()
163 w.releaseDevice(w.Pointer)
164 w.Pointer = nil
165 }
166 if ev.Capabilities&2 == 0 && w.WLKeyboard != nil {
167 w.releaseDevice(w.WLKeyboard)
168 w.WLKeyboard = nil
169 w.post(Event{Kind: InputFocusOut})
170 }
171 if ev.Capabilities&1 != 0 && w.Pointer == nil {
172 p, err := w.Seat.GetPointer()
173 if err != nil {
174 return err
175 }
176 w.Pointer = p
177 w.watchPointer(p)
178 }
179 if ev.Capabilities&2 != 0 && w.WLKeyboard == nil {
180 k, err := w.Seat.GetKeyboard()
181 if err != nil {
182 return err
183 }
184 w.WLKeyboard = k
185 w.watchKeyboard(k)
186 }
187 return nil
188 }
189 return fmt.Errorf("not a seat event: %d", ev.Kind)
190}
191
192// releaseDevice destroys a wl_pointer or wl_keyboard. Their release request
193// exists since seat version 3; older seats only forget the proxy locally.
194func (w *Window) releaseDevice(p interface {
195 wl.Proxy
196 Release() error
197}) {
198 if w.seatVersion >= 3 {
199 _ = p.Release()
200 } else if c := p.Context(); c != nil {
201 c.Unregister(p)
202 }
203}
204func (w *Window) releaseSeat() error {
205 if w.Pointer != nil {
206 w.dropCursorDevice()
207 w.releaseDevice(w.Pointer)
208 w.Pointer = nil
209 }
210 if w.WLKeyboard != nil {
211 w.releaseDevice(w.WLKeyboard)
212 w.WLKeyboard = nil
213 }
214 if w.Seat != nil {
215 if w.seatVersion >= 5 {
216 _ = w.Seat.Release()
217 } else {
218 w.Display.Context().Unregister(w.Seat)
219 }
220 w.Seat = nil
221 }
222 return nil
223}
224func (w *Window) watchPointer(p *core.Pointer) {
225 var frame []Input // only accessed by the reader
226 var x, y float64
227 var inside bool
228 var source uint32
229 // The generation tells a late enter from a released pointer apart from
230 // the current one; object IDs can be reused.
231 w.pointerGen++
232 surfaceID, version, gen := w.Surface.ID(), w.seatVersion, w.pointerGen
233 flush := func() {
234 if len(frame) > 0 {
235 w.post(Event{Kind: InputFrame, Inputs: frame})
236 frame = nil
237 }
238 source = 0
239 }
240 add := func(i Input) {
241 i.AxisSource = source
242 frame = append(frame, i)
243 if version < 5 {
244 flush()
245 }
246 }
247 p.OnEnter(func(serial uint32, id uint32, px, py wl.Fixed) {
248 if id != surfaceID {
249 return
250 }
251 inside = true
252 x, y = px.Float64(), py.Float64()
253 add(Input{Kind: "motion", X: x, Y: y, Enter: true, Serial: serial, PointerGen: gen})
254 })
255 p.OnLeave(func(_ uint32, id uint32) {
256 if id != surfaceID {
257 return
258 }
259 inside = false
260 add(Input{Kind: "leave"})
261 })
262 p.OnMotion(func(_ uint32, px, py wl.Fixed) {
263 if inside {
264 x, y = px.Float64(), py.Float64()
265 add(Input{Kind: "motion", X: x, Y: y})
266 }
267 })
268 p.OnButton(func(serial, _, button, state uint32) {
269 if !inside && state == 1 {
270 return
271 }
272 kind := "release"
273 if state == 1 {
274 kind = "press"
275 }
276 add(Input{Kind: kind, X: x, Y: y, Button: button, Serial: serial})
277 })
278 p.OnAxisSource(func(s uint32) { source = s })
279 p.OnAxis(func(_, axis uint32, v wl.Fixed) {
280 if inside {
281 i := Input{Kind: "axis", X: x, Y: y, Axis: axis}
282 if axis == 0 {
283 i.DY = v.Float64()
284 } else {
285 i.DX = v.Float64()
286 }
287 add(i)
288 }
289 })
290 p.OnAxisValue120(func(axis uint32, v int32) {
291 if inside {
292 add(Input{Kind: "axis", X: x, Y: y, Axis: axis, Value120: v})
293 }
294 })
295 p.OnAxisStop(func(_, axis uint32) {
296 if inside {
297 add(Input{Kind: "axis", X: x, Y: y, Axis: axis, AxisStop: true})
298 }
299 })
300 p.OnFrame(flush)
301}
302func (w *Window) watchKeyboard(k *core.Keyboard) {
303 surfaceID := w.Surface.ID()
304 k.OnKeymap(func(format uint32, fd *wl.OwnedFD, size uint32) {
305 n, err := fd.Take()
306 if err != nil {
307 w.post(Event{Kind: TransportError, Err: err})
308 return
309 }
310 w.postFD(Event{Kind: InputKeymap, FD: n, Format: format, Size: size})
311 })
312 k.OnEnter(func(serial, id uint32, _ []byte) {
313 if id == surfaceID {
314 w.post(Event{Kind: InputFocusIn, Serial: serial})
315 }
316 })
317 k.OnLeave(func(_, id uint32) {
318 if id == surfaceID {
319 w.post(Event{Kind: InputFocusOut})
320 }
321 })
322 k.OnKey(func(serial, _, key, state uint32) {
323 w.post(Event{Kind: InputKey, Serial: serial, Code: key, Pressed: state == 1})
324 })
325 k.OnModifiers(func(_, depressed, latched, locked, group uint32) {
326 w.post(Event{Kind: InputModifiers, Mods: [4]uint32{depressed, latched, locked, group}})
327 })
328 k.OnRepeatInfo(func(rate, delay int32) { w.post(Event{Kind: InputRepeatInfo, Rate: rate, Delay: delay}) })
329}
330
331// InputEvent is a loop-owned input value. Runtime integration drains these later.
332type InputEvent = Input
333
334// DrainInput appends pending input in order to dst and clears the queue.
335// The queue holds at most 4096 events. Motion is evicted first, compatible
336// adjacent axes are merged; if neither is possible it emits InputReset and
337// starts a new queue. InputOverflow counts discarded events. Call on the Apply loop.
338func (w *Window) DrainInput(dst []InputEvent) []InputEvent {
339 dst = append(dst, w.inputQueue...)
340 clear(w.inputQueue)
341 w.inputQueue = w.inputQueue[:0]
342 return dst
343}
344
345func mergeAxis(dst *Input, src Input) bool {
346 if dst.Kind != "axis" || src.Kind != "axis" || dst.Axis != src.Axis ||
347 dst.AxisSource != src.AxisSource || dst.AxisStop || src.AxisStop {
348 return false
349 }
350 dst.DX += src.DX
351 dst.DY += src.DY
352 dst.Value120 += src.Value120
353 dst.X, dst.Y = src.X, src.Y
354 return true
355}
356
357func (w *Window) queueInput(inputs []Input) {
358 for _, input := range inputs {
359 if len(w.inputQueue) == 4096 {
360 index := -1
361 for i, v := range w.inputQueue {
362 // An enter motion carries the serial cursor shapes need.
363 if v.Kind == "motion" && !v.Enter {
364 index = i
365 break
366 }
367 }
368 if index < 0 {
369 // Adjacent axes on the same axis/source can be combined without
370 // crossing a button or key event or losing scroll distance.
371 for i := 0; i+1 < len(w.inputQueue); i++ {
372 if mergeAxis(&w.inputQueue[i], w.inputQueue[i+1]) {
373 index = i + 1
374 break
375 }
376 }
377 }
378 if index >= 0 {
379 if w.inputQueue[index].Kind == "motion" {
380 w.InputOverflow++
381 }
382 copy(w.inputQueue[index:], w.inputQueue[index+1:])
383 w.inputQueue = w.inputQueue[:len(w.inputQueue)-1]
384 } else {
385 // A previous reset is still pending if the consumer has not
386 // drained yet; keep one marker across successive overflows.
387 dropped := len(w.inputQueue)
388 if w.inputQueue[0].Kind == InputReset {
389 dropped--
390 }
391 w.InputOverflow += uint64(dropped)
392 // Keep the latest discarded enter or leave: it still decides
393 // whether the pointer is inside, and an enter carries the serial
394 // cursor shapes need.
395 var focus *Input
396 for i := range w.inputQueue {
397 if v := w.inputQueue[i]; v.Enter || v.Kind == "leave" {
398 focus = &v
399 }
400 }
401 clear(w.inputQueue)
402 w.inputQueue = w.inputQueue[:0]
403 w.inputQueue = append(w.inputQueue, Input{Kind: InputReset})
404 if focus != nil {
405 w.inputQueue = append(w.inputQueue, *focus)
406 }
407 // Discarded presses must not keep repeating on the loop.
408 w.inputAdapter.resetHeld()
409 }
410 }
411 w.inputQueue = append(w.inputQueue, input)
412 }
413}