1package wayland
2
3import (
4 "github.com/bnema/go-wayland-bindings/server/inputmethod"
5 "github.com/bnema/go-wayland-bindings/server/textinput"
6 "github.com/bnema/go-wayland-bindings/server/wayland"
7 "github.com/bnema/neferwl/internal/ports"
8 "github.com/bnema/purego-libwayland/server"
9)
10
11// Text input (zwp_text_input_v3) lets clients take IME text; one input
12// method (zwp_input_method_v2, e.g. fcitx5 or IBus) composes it. Text
13// input focus follows keyboard focus. The first focused text input that
14// commits enable is the active one: the input method serves only it.
15
16// maxIMEText is the spec limit for strings carried by both protocols.
17const maxIMEText = 4000
18
19// textState is the double-buffered state of a text input.
20type textState struct {
21 enabled bool
22 surrounding string
23 cursor, anchor uint32
24 hasSurrounding bool
25 cause uint32
26 hint, purpose uint32
27 rect ports.Rect
28 hasRect bool
29}
30
31type textInput struct {
32 server *Server
33 res *textinput.ZwpTextInputV3
34 client server.Client
35 // focus is the entered surface, nil after leave.
36 focus *wayland.Surface
37 pending, current textState
38 // toggled: an enable or disable request is pending.
39 toggled bool
40 // serial counts commit requests, the serial of done events.
41 serial uint32
42}
43
44func registerTextInput(d *server.Display, s *Server) error {
45 return textinput.NewZwpTextInputManagerV3Global(d, 1, func(c server.Client, v, id uint32) {
46 _, _ = textinput.NewZwpTextInputManagerV3(c, int32(v), id, textInputManager{s})
47 })
48}
49
50type textInputManager struct{ server *Server }
51
52func (textInputManager) Destroy(*textinput.ZwpTextInputManagerV3) {}
53
54func (m textInputManager) GetTextInput(r *textinput.ZwpTextInputManagerV3, id uint32, _ *wayland.Seat) {
55 s := m.server
56 t := &textInput{server: s, client: r.Client()}
57 res, err := textinput.NewZwpTextInputV3(r.Client(), r.Version(), id, t)
58 if err != nil {
59 return
60 }
61 t.res = res
62 s.textInputs = append(s.textInputs, t)
63 res.OnDestroy = func() {
64 s.textInputs = removeItem(s.textInputs, t)
65 if s.activeText == t {
66 s.deactivateText()
67 }
68 t.focus = nil
69 }
70 if surf, _ := s.focusTarget(s.seat.focused); surf != nil && surf.Client() == t.client {
71 t.enter(surf)
72 }
73}
74
75func (*textInput) Destroy(*textinput.ZwpTextInputV3) {}
76
77// Enable resets the pending state, as the spec asks.
78func (t *textInput) Enable(*textinput.ZwpTextInputV3) {
79 t.pending, t.toggled = textState{enabled: true}, true
80}
81
82// Disable invalidates the pending state.
83func (t *textInput) Disable(*textinput.ZwpTextInputV3) {
84 t.pending, t.toggled = textState{}, true
85}
86
87// SetSurroundingText ignores text past the spec limit or a cursor or
88// anchor outside it.
89func (t *textInput) SetSurroundingText(_ *textinput.ZwpTextInputV3, text string, cursor, anchor int32) {
90 if t.server.protected() {
91 return
92 }
93 if len(text) > maxIMEText || cursor < 0 || anchor < 0 || int(cursor) > len(text) || int(anchor) > len(text) {
94 t.server.log.Debug().Int("bytes", len(text)).Msg("surrounding text ignored")
95 return
96 }
97 p := &t.pending
98 p.surrounding, p.cursor, p.anchor, p.hasSurrounding = text, uint32(cursor), uint32(anchor), true
99}
100
101func (t *textInput) SetTextChangeCause(_ *textinput.ZwpTextInputV3, cause uint32) {
102 t.pending.cause = cause
103}
104
105func (t *textInput) SetContentType(_ *textinput.ZwpTextInputV3, hint, purpose uint32) {
106 t.pending.hint, t.pending.purpose = hint, purpose
107}
108
109func (t *textInput) SetCursorRectangle(_ *textinput.ZwpTextInputV3, x, y, w, h int32) {
110 t.pending.rect, t.pending.hasRect = ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}, true
111}
112
113// Commit applies the pending state. Every commit counts for the done
114// serial, but after leave the state is ignored until the next enter.
115func (t *textInput) Commit(*textinput.ZwpTextInputV3) {
116 t.serial++
117 if t.server.protected() {
118 t.pending, t.current, t.toggled = textState{}, textState{}, false
119 return
120 }
121 if t.focus == nil {
122 return
123 }
124 reenabled := t.toggled && t.pending.enabled
125 t.current = t.pending
126 t.pending.cause = uint32(textinput.ZwpTextInputV3ChangeCauseInputMethod)
127 t.toggled = false
128 t.server.textCommitted(t, reenabled)
129}
130
131// Version 2 requests: the global is version 1.
132func (*textInput) SetAvailableActions(*textinput.ZwpTextInputV3, []byte) {}
133func (*textInput) ShowInputPanel(*textinput.ZwpTextInputV3) {}
134func (*textInput) HideInputPanel(*textinput.ZwpTextInputV3) {}
135
136// enter focuses the text input on surf; its state starts over.
137func (t *textInput) enter(surf *wayland.Surface) {
138 if t.server.protected() {
139 return
140 }
141 t.focus = surf
142 t.pending, t.current, t.toggled = textState{}, textState{}, false
143 t.res.SendEnter(surf)
144}
145
146// leave clears the focus; a destroyed surface gets no leave event.
147func (t *textInput) leave() {
148 s := t.server
149 if s.activeText == t {
150 s.deactivateText()
151 }
152 if t.focus.Resource.Alive() {
153 t.res.SendLeave(t.focus)
154 }
155 t.focus = nil
156 t.pending, t.current, t.toggled = textState{}, textState{}, false
157}
158
159// textCommitted activates, updates or deactivates the input method for a
160// committed text input. Enabling a second text input is ignored.
161func (s *Server) textCommitted(t *textInput, reenabled bool) {
162 if s.protected() {
163 return
164 }
165 switch {
166 case t.current.enabled && s.activeText == nil:
167 s.activeText = t
168 s.activateIM()
169 case t.current.enabled && s.activeText == t:
170 if reenabled {
171 s.activateIM()
172 } else {
173 s.sendIMState()
174 }
175 case !t.current.enabled && s.activeText == t:
176 s.deactivateText()
177 }
178}
179
180// textInputFocus moves text input focus to the keyboard focus: leave for
181// text inputs on another surface, then enter for those of the focused
182// client, as the spec orders them.
183func (s *Server) textInputFocus() {
184 surf, _ := s.focusTarget(s.seat.focused)
185 for _, t := range s.textInputs {
186 if t.focus != nil && (surf == nil || t.focus.Resource != surf.Resource) {
187 t.leave()
188 }
189 }
190 if surf == nil {
191 return
192 }
193 for _, t := range s.textInputs {
194 if t.focus == nil && t.client == surf.Client() {
195 t.enter(surf)
196 }
197 }
198}
199
200// deactivateText drops the active text input and tells the input method.
201func (s *Server) deactivateText() {
202 s.activeText = nil
203 im := s.inputMethod
204 if im == nil {
205 return
206 }
207 im.res.SendDeactivate()
208 im.done()
209}
210
211// activateIM sends activate and the active text input's state.
212func (s *Server) activateIM() {
213 im := s.inputMethod
214 if s.protected() || im == nil || s.activeText == nil {
215 return
216 }
217 im.pending = imState{}
218 im.activated = im.dones
219 im.res.SendActivate()
220 s.sendIMState()
221}
222
223// sendIMState sends the active text input's state, then done.
224func (s *Server) sendIMState() {
225 im, t := s.inputMethod, s.activeText
226 if s.protected() || im == nil || t == nil {
227 return
228 }
229 c := t.current
230 if c.hasSurrounding {
231 im.res.SendSurroundingText(c.surrounding, c.cursor, c.anchor)
232 }
233 im.res.SendTextChangeCause(c.cause)
234 im.res.SendContentType(c.hint, c.purpose)
235 for _, p := range im.popups {
236 p.sendRect()
237 }
238 im.done()
239}
240
241// imState is the double-buffered state of the input method.
242type imState struct {
243 preedit string
244 begin, end int32
245 hasPreedit bool
246 commit string
247 hasCommit bool
248 before, after uint32
249}
250
251type inputMethod struct {
252 server *Server
253 res *inputmethod.ZwpInputMethodV2
254 // inert: another input method was bound first; requests are ignored.
255 inert bool
256 dones uint32
257 // activated is dones when the last activate was sent: a commit with
258 // a serial up to it predates the activation.
259 activated uint32
260 pending imState
261 grab *keyboardGrab
262 popups []*inputPopup
263}
264
265func registerInputMethod(d *server.Display, s *Server) error {
266 return inputmethod.NewZwpInputMethodManagerV2Global(d, 1, func(c server.Client, v, id uint32) {
267 _, _ = inputmethod.NewZwpInputMethodManagerV2(c, int32(v), id, inputMethodManager{s})
268 })
269}
270
271type inputMethodManager struct{ server *Server }
272
273func (inputMethodManager) Destroy(*inputmethod.ZwpInputMethodManagerV2) {}
274
275// GetInputMethod makes the first input method the seat's; later ones get
276// unavailable until it is gone.
277func (m inputMethodManager) GetInputMethod(r *inputmethod.ZwpInputMethodManagerV2, _ *wayland.Seat, id uint32) {
278 s := m.server
279 im := &inputMethod{server: s, inert: s.inputMethod != nil}
280 res, err := inputmethod.NewZwpInputMethodV2(r.Client(), r.Version(), id, im)
281 if err != nil {
282 return
283 }
284 im.res = res
285 if im.inert {
286 res.SendUnavailable()
287 s.log.Debug().Int("pid", r.Client().PID()).Msg("input method unavailable")
288 return
289 }
290 s.inputMethod = im
291 res.OnDestroy = func() { s.inputMethodGone(im) }
292 s.log.Info().Int("pid", r.Client().PID()).Msg("input method bound")
293 s.activateIM()
294}
295
296// inputMethodGone frees the seat slot. A grab ends; the active text
297// input's preedit is cleared by an empty done.
298func (s *Server) inputMethodGone(im *inputMethod) {
299 if s.inputMethod != im {
300 return
301 }
302 s.inputMethod = nil
303 if im.grab != nil {
304 im.grab = nil
305 s.grabEnded()
306 }
307 if t := s.activeText; t != nil {
308 t.res.SendDone(t.serial)
309 }
310 s.log.Info().Msg("input method gone")
311}
312
313// done sends done and counts it for commit serials.
314func (im *inputMethod) done() {
315 im.res.SendDone()
316 im.dones++
317}
318
319func (im *inputMethod) CommitString(_ *inputmethod.ZwpInputMethodV2, text string) {
320 if im.server.protected() || im.inert || !im.fits(text) {
321 return
322 }
323 im.pending.commit, im.pending.hasCommit = text, true
324}
325
326func (im *inputMethod) SetPreeditString(_ *inputmethod.ZwpInputMethodV2, text string, begin, end int32) {
327 if im.server.protected() || im.inert || !im.fits(text) {
328 return
329 }
330 p := &im.pending
331 p.preedit, p.begin, p.end, p.hasPreedit = text, begin, end, true
332}
333
334func (im *inputMethod) fits(text string) bool {
335 if len(text) > maxIMEText {
336 im.server.log.Debug().Int("bytes", len(text)).Msg("input method text too long")
337 return false
338 }
339 return true
340}
341
342func (im *inputMethod) DeleteSurroundingText(_ *inputmethod.ZwpInputMethodV2, before, after uint32) {
343 if im.server.protected() || im.inert {
344 return
345 }
346 im.pending.before, im.pending.after = before, after
347}
348
349// Commit sends the pending state to the active text input, in the
350// spec's order, with done carrying the text input's commit count.
351// A commit from before the last activate is dropped: its text belongs to
352// the previous text input. Within one activation, like wlroots, a stale
353// serial is only logged: dropping the commit would lose typed text.
354func (im *inputMethod) Commit(_ *inputmethod.ZwpInputMethodV2, serial uint32) {
355 if im.inert {
356 return
357 }
358 st := im.pending
359 im.pending = imState{}
360 s := im.server
361 if s.protected() {
362 return
363 }
364 t := s.activeText
365 if t == nil {
366 return
367 }
368 if serial <= im.activated {
369 s.log.Debug().Uint32("serial", serial).Uint32("activated", im.activated).Msg("input method commit before activation dropped")
370 return
371 }
372 if serial != im.dones {
373 s.log.Debug().Uint32("serial", serial).Uint32("done", im.dones).Msg("input method commit with stale serial")
374 }
375 if st.hasPreedit {
376 t.res.SendPreeditString(st.preedit, st.begin, st.end)
377 }
378 if st.hasCommit {
379 t.res.SendCommitString(st.commit)
380 }
381 if st.before != 0 || st.after != 0 {
382 t.res.SendDeleteSurroundingText(st.before, st.after)
383 }
384 t.res.SendDone(t.serial)
385}
386
387// GetInputPopupSurface gives a surface the input_popup role. The popup
388// learns the text cursor rectangle; it is not drawn.
389func (im *inputMethod) GetInputPopupSurface(r *inputmethod.ZwpInputMethodV2, id uint32, surf *wayland.Surface) {
390 s := im.server
391 if im.inert {
392 // An unavailable input method's requests have no effect.
393 _, _ = inputmethod.NewZwpInputPopupSurfaceV2(r.Client(), r.Version(), id, &inputPopup{im: im})
394 return
395 }
396 if surf == nil {
397 return
398 }
399 state := s.surfaces[surf.Resource]
400 if state == nil {
401 return
402 }
403 if state.kind != roleNone {
404 r.PostError(uint32(inputmethod.ZwpInputMethodV2ErrorRole), "surface already has a role")
405 return
406 }
407 p := &inputPopup{im: im}
408 res, err := inputmethod.NewZwpInputPopupSurfaceV2(r.Client(), r.Version(), id, p)
409 if err != nil {
410 return
411 }
412 state.kind, p.res = roleInputPopup, res
413 im.popups = append(im.popups, p)
414 res.OnDestroy = func() { im.popups = removeItem(im.popups, p) }
415 if s.activeText != nil {
416 p.sendRect()
417 }
418}
419
420// GrabKeyboard routes seat keys to the input method. A second grab
421// while one is held stays inert.
422func (im *inputMethod) GrabKeyboard(r *inputmethod.ZwpInputMethodV2, id uint32) {
423 g := &keyboardGrab{}
424 res, err := inputmethod.NewZwpInputMethodKeyboardGrabV2(r.Client(), r.Version(), id, g)
425 if err != nil || im.server.protected() || im.inert || im.grab != nil {
426 return
427 }
428 g.res, im.grab = res, g
429 res.OnDestroy = func() {
430 if im.grab == g {
431 im.grab = nil
432 im.server.grabEnded()
433 }
434 }
435 s := im.server
436 if s.seat.keymapFD >= 0 {
437 res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
438 }
439 res.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
440 s.serial++
441 m := s.seat.modState
442 res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
443 s.log.Debug().Msg("input method grabbed keyboard")
444}
445
446func (*inputMethod) Destroy(*inputmethod.ZwpInputMethodV2) {}
447
448// inputPopup is an input method's candidate surface.
449type inputPopup struct {
450 im *inputMethod
451 res *inputmethod.ZwpInputPopupSurfaceV2
452}
453
454func (*inputPopup) Destroy(*inputmethod.ZwpInputPopupSurfaceV2) {}
455
456// sendRect tells the popup where the active text cursor is.
457func (p *inputPopup) sendRect() {
458 if p.im.server.protected() {
459 return
460 }
461 t := p.im.server.activeText
462 if t == nil || !t.current.hasRect {
463 return
464 }
465 r := t.current.rect
466 p.res.SendTextInputRectangle(int32(r.X), int32(r.Y), int32(r.W), int32(r.H))
467}
468
469type keyboardGrab struct {
470 res *inputmethod.ZwpInputMethodKeyboardGrabV2
471}
472
473func (*keyboardGrab) Release(*inputmethod.ZwpInputMethodKeyboardGrabV2) {}
474
475// grab is the live keyboard grab, nil without one.
476func (s *Server) grab() *keyboardGrab {
477 if s.protected() {
478 return nil
479 }
480 if im := s.inputMethod; im != nil {
481 return im.grab
482 }
483 return nil
484}
485
486// grabKey sends a focused key to the input method grab and reports
487// whether it consumed it. The release of a key the client got pressed
488// goes to the client; the release of a key the grab got pressed never
489// does, even after the grab ends.
490func (s *Server) grabKey(c ports.ForwardKey) bool {
491 code := c.Key.Keycode
492 if s.protected() {
493 if !c.Key.Pressed && s.seat.grabKeys[code] {
494 delete(s.seat.grabKeys, code)
495 return true
496 }
497 return false // protected seat routing belongs to the parent
498 }
499 g := s.grab()
500 owned := !c.Key.Pressed && s.seat.grabKeys[code]
501 if owned {
502 delete(s.seat.grabKeys, code)
503 if g == nil {
504 s.grabReleaseDropped(c.Key.State)
505 return true
506 }
507 } else if g == nil || c.ID != s.seat.focused || !c.Key.Pressed && s.seat.heldKeys[code] {
508 return false
509 }
510 state := uint32(0)
511 if c.Key.Pressed {
512 state = 1
513 if s.seat.grabKeys == nil {
514 s.seat.grabKeys = make(map[uint32]bool)
515 }
516 s.seat.grabKeys[code] = true
517 }
518 s.serial++
519 g.res.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
520 if c.Key.State != s.seat.modState {
521 s.seat.modState = c.Key.State
522 m := s.seat.modState
523 s.serial++
524 g.res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
525 }
526 return true
527}
528
529// grabReleaseDropped keeps the focused client's modifiers right when the
530// release of a grab-owned key is dropped.
531func (s *Server) grabReleaseDropped(m ports.ModState) {
532 if s.protected() {
533 return
534 }
535 if m == s.seat.modState {
536 return
537 }
538 s.seat.modState = m
539 s.serial++
540 for _, k := range s.clientKeyboards(s.focusClient()) {
541 s.sendModifiers(k)
542 }
543}
544
545// grabEnded gives the focused client the seat modifiers it missed.
546func (s *Server) grabEnded() {
547 if s.protected() {
548 return
549 }
550 s.serial++
551 for _, k := range s.clientKeyboards(s.focusClient()) {
552 s.sendModifiers(k)
553 }
554 s.log.Debug().Msg("input method keyboard grab ended")
555}
556
557// sendGrabKeymap updates a grab after a seat keymap or repeat change.
558func (s *Server) sendGrabKeymap(keymap bool) {
559 g := s.grab()
560 if g == nil {
561 return
562 }
563 if keymap && s.seat.keymapFD >= 0 {
564 g.res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
565 }
566 g.res.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
567}