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 && w.connection == nil {
154 w.ownerFocusOut()
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 if w.keymapCache != nil {
170 w.keymapCache.close()
171 w.keymapCache = nil
172 }
173 if w.connection == nil {
174 w.ownerFocusOut()
175 }
176 }
177 if ev.Capabilities&1 != 0 && w.Pointer == nil {
178 p, err := w.Seat.GetPointer()
179 if err != nil {
180 return err
181 }
182 w.Pointer = p
183 w.watchPointer(p)
184 }
185 if ev.Capabilities&2 != 0 && w.WLKeyboard == nil {
186 k, err := w.Seat.GetKeyboard()
187 if err != nil {
188 return err
189 }
190 w.WLKeyboard = k
191 w.watchKeyboard(k)
192 }
193 return nil
194 }
195 return fmt.Errorf("not a seat event: %d", ev.Kind)
196}
197
198// releaseDevice destroys a wl_pointer or wl_keyboard. Their release request
199// exists since seat version 3; older seats only forget the proxy locally.
200func (w *Window) releaseDevice(p interface {
201 wl.Proxy
202 Release() error
203}) {
204 if w.seatVersion >= 3 {
205 _ = p.Release()
206 } else if c := p.Context(); c != nil {
207 c.Unregister(p)
208 }
209}
210
211// TakeInputEvents transfers lock-only raw input ownership, including keymap
212// FDs. The secret owner must close each FD after consuming it. No Text/Key
213// translation occurs here; ordinary windows continue to use Inputs().
214func (w *Window) TakeInputEvents() []Event {
215 events := w.rawInput
216 w.rawInput = nil
217 return events
218}
219
220func (w *Window) ownerFocusOut() {
221 if w.LockSurface != nil {
222 for _, ev := range w.rawInput {
223 CloseEventFD(ev)
224 }
225 clear(w.rawInput)
226 w.rawInput = append(w.rawInput[:0], Event{Kind: InputFocusOut, Window: w})
227 return
228 }
229 if w.inputAdapter != nil {
230 w.inputAdapter.Keyboard.Focus(false)
231 w.inputAdapter.focused = false
232 w.inputAdapter.serial = 0
233 }
234 w.queueInput([]Input{{Kind: "focus-out"}})
235}
236
237func (w *Window) releaseSeat() error {
238 if w.keymapCache != nil {
239 w.keymapCache.close()
240 w.keymapCache = nil
241 }
242 if w.Pointer != nil {
243 w.dropCursorDevice()
244 w.releaseDevice(w.Pointer)
245 w.Pointer = nil
246 }
247 if w.WLKeyboard != nil {
248 w.releaseDevice(w.WLKeyboard)
249 w.WLKeyboard = nil
250 }
251 if w.Seat != nil {
252 if w.seatVersion >= 5 {
253 _ = w.Seat.Release()
254 } else {
255 w.Display.Context().Unregister(w.Seat)
256 }
257 w.Seat = nil
258 }
259 return nil
260}
261func (w *Window) watchPointer(p *core.Pointer) {
262 if w.connection != nil {
263 w.watchSharedPointer(p)
264 return
265 }
266 var frame []Input // only accessed by the reader
267 var x, y float64
268 var inside bool
269 var source uint32
270 // The generation tells a late enter from a released pointer apart from
271 // the current one; object IDs can be reused.
272 w.pointerGen++
273 surfaceID, version, gen := w.Surface.ID(), w.seatVersion, w.pointerGen
274 flush := func() {
275 if len(frame) > 0 {
276 w.post(Event{Kind: InputFrame, Inputs: frame})
277 frame = nil
278 }
279 source = 0
280 }
281 add := func(i Input) {
282 i.AxisSource = source
283 frame = append(frame, i)
284 if version < 5 {
285 flush()
286 }
287 }
288 p.OnEnter(func(serial uint32, id uint32, px, py wl.Fixed) {
289 if id != surfaceID {
290 return
291 }
292 inside = true
293 x, y = px.Float64(), py.Float64()
294 add(Input{Kind: "motion", X: x, Y: y, Enter: true, Serial: serial, PointerGen: gen})
295 })
296 p.OnLeave(func(_ uint32, id uint32) {
297 if id != surfaceID {
298 return
299 }
300 inside = false
301 add(Input{Kind: "leave"})
302 })
303 p.OnMotion(func(_ uint32, px, py wl.Fixed) {
304 if inside {
305 x, y = px.Float64(), py.Float64()
306 add(Input{Kind: "motion", X: x, Y: y})
307 }
308 })
309 p.OnButton(func(serial, _, button, state uint32) {
310 if !inside && state == 1 {
311 return
312 }
313 kind := "release"
314 if state == 1 {
315 kind = "press"
316 }
317 add(Input{Kind: kind, X: x, Y: y, Button: button, Serial: serial})
318 })
319 p.OnAxisSource(func(s uint32) { source = s })
320 p.OnAxis(func(_, axis uint32, v wl.Fixed) {
321 if inside {
322 i := Input{Kind: "axis", X: x, Y: y, Axis: axis}
323 if axis == 0 {
324 i.DY = v.Float64()
325 } else {
326 i.DX = v.Float64()
327 }
328 add(i)
329 }
330 })
331 p.OnAxisValue120(func(axis uint32, v int32) {
332 if inside {
333 add(Input{Kind: "axis", X: x, Y: y, Axis: axis, Value120: v})
334 }
335 })
336 p.OnAxisStop(func(_, axis uint32) {
337 if inside {
338 add(Input{Kind: "axis", X: x, Y: y, Axis: axis, AxisStop: true})
339 }
340 })
341 p.OnFrame(flush)
342}
343func (w *Window) watchKeyboard(k *core.Keyboard) {
344 if w.connection != nil {
345 w.watchSharedKeyboard(k)
346 return
347 }
348 surfaceID := w.Surface.ID()
349 k.OnKeymap(func(format uint32, fd *wl.OwnedFD, size uint32) {
350 n, err := fd.Take()
351 if err != nil {
352 w.post(Event{Kind: TransportError, Err: err})
353 return
354 }
355 w.postFD(Event{Kind: InputKeymap, FD: n, Format: format, Size: size})
356 })
357 k.OnEnter(func(serial, id uint32, _ []byte) {
358 if id == surfaceID {
359 w.post(Event{Kind: InputFocusIn, Serial: serial})
360 }
361 })
362 k.OnLeave(func(_, id uint32) {
363 if id == surfaceID {
364 w.post(Event{Kind: InputFocusOut})
365 }
366 })
367 k.OnKey(func(serial, _, key, state uint32) {
368 w.post(Event{Kind: InputKey, Serial: serial, Code: key, Pressed: state == 1})
369 })
370 k.OnModifiers(func(_, depressed, latched, locked, group uint32) {
371 w.post(Event{Kind: InputModifiers, Mods: [4]uint32{depressed, latched, locked, group}})
372 })
373 k.OnRepeatInfo(func(rate, delay int32) { w.post(Event{Kind: InputRepeatInfo, Rate: rate, Delay: delay}) })
374}
375
376// InputEvent is a loop-owned input value. Runtime integration drains these later.
377type InputEvent = Input
378
379// DrainInput appends pending input in order to dst and clears the queue.
380// The queue holds at most 4096 events. Motion is evicted first, compatible
381// adjacent axes are merged; if neither is possible it emits InputReset and
382// starts a new queue. InputOverflow counts discarded events. Call on the Apply loop.
383func (w *Window) DrainInput(dst []InputEvent) []InputEvent {
384 dst = append(dst, w.inputQueue...)
385 clear(w.inputQueue)
386 w.inputQueue = w.inputQueue[:0]
387 return dst
388}
389
390func mergeAxis(dst *Input, src Input) bool {
391 if dst.Kind != "axis" || src.Kind != "axis" || dst.Axis != src.Axis ||
392 dst.AxisSource != src.AxisSource || dst.AxisStop || src.AxisStop {
393 return false
394 }
395 dst.DX += src.DX
396 dst.DY += src.DY
397 dst.Value120 += src.Value120
398 dst.X, dst.Y = src.X, src.Y
399 return true
400}
401
402func (w *Window) queueInput(inputs []Input) {
403 for _, input := range inputs {
404 if len(w.inputQueue) == 4096 {
405 index := -1
406 for i, v := range w.inputQueue {
407 // An enter motion carries the serial cursor shapes need.
408 if v.Kind == "motion" && !v.Enter {
409 index = i
410 break
411 }
412 }
413 if index < 0 {
414 // Adjacent axes on the same axis/source can be combined without
415 // crossing a button or key event or losing scroll distance.
416 for i := 0; i+1 < len(w.inputQueue); i++ {
417 if mergeAxis(&w.inputQueue[i], w.inputQueue[i+1]) {
418 index = i + 1
419 break
420 }
421 }
422 }
423 if index >= 0 {
424 if w.inputQueue[index].Kind == "motion" {
425 w.InputOverflow++
426 }
427 copy(w.inputQueue[index:], w.inputQueue[index+1:])
428 w.inputQueue = w.inputQueue[:len(w.inputQueue)-1]
429 } else {
430 // A previous reset is still pending if the consumer has not
431 // drained yet; keep one marker across successive overflows.
432 dropped := len(w.inputQueue)
433 if w.inputQueue[0].Kind == InputReset {
434 dropped--
435 }
436 w.InputOverflow += uint64(dropped)
437 // Keep the latest discarded enter or leave: it still decides
438 // whether the pointer is inside, and an enter carries the serial
439 // cursor shapes need.
440 var focus *Input
441 for i := range w.inputQueue {
442 if v := w.inputQueue[i]; v.Enter || v.Kind == "leave" {
443 focus = &v
444 }
445 }
446 clear(w.inputQueue)
447 w.inputQueue = w.inputQueue[:0]
448 w.inputQueue = append(w.inputQueue, Input{Kind: InputReset})
449 if focus != nil {
450 w.inputQueue = append(w.inputQueue, *focus)
451 }
452 // Discarded presses must not keep repeating on the loop.
453 w.inputAdapter.resetHeld()
454 }
455 }
456 w.inputQueue = append(w.inputQueue, input)
457 }
458}