From d5424ef7fc291ac52e12f1984de22398f0866027 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 22 Nov 2019 18:27:29 +0100 Subject: [PATCH] app/internal/window: replace X11 input handling with xkb Unifies Wayland and X11 keyboard handling. Signed-off-by: Elias Naur --- app/internal/window/os_wayland.go | 25 ++- app/internal/window/os_x11.go | 267 +++++++++--------------------- app/internal/xkb/xkb_unix.go | 104 +++++++----- 3 files changed, 156 insertions(+), 240 deletions(-) diff --git a/app/internal/window/os_wayland.go b/app/internal/window/os_wayland.go index 1ed9d43d..58380aa1 100644 --- a/app/internal/window/os_wayland.go +++ b/app/internal/window/os_wayland.go @@ -628,18 +628,14 @@ func (w *window) resetFling() { func gio_onKeyboardKeymap(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, format C.uint32_t, fd C.int32_t, size C.uint32_t) { defer syscall.Close(int(fd)) conn.repeat.Stop(0) - if conn.xkb != nil { - conn.xkb.Destroy() - } + conn.xkb.DestroyKeymapState() if format != C.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 { return } - xkb, err := xkb.New(int(format), int(fd), int(size)) - if err != nil { + if err := conn.xkb.LoadKeymap(int(format), int(fd), int(size)); err != nil { // TODO: Do better. panic(err) } - conn.xkb = xkb } //export gio_onKeyboardEnter @@ -663,10 +659,10 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri w := winMap[keyboard] w.resetFling() conn.repeat.Stop(t) - if state != C.WL_KEYBOARD_KEY_STATE_PRESSED || conn.xkb == nil { + if state != C.WL_KEYBOARD_KEY_STATE_PRESSED { return } - kc := uint32(keyCode) + kc := mapXKBKeycode(uint32(keyCode)) for _, e := range conn.xkb.DispatchKey(kc) { w.w.Event(e) } @@ -675,6 +671,11 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri } } +func mapXKBKeycode(keyCode uint32) uint32 { + // According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode." + return keyCode + 8 +} + func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) { if r.rate <= 0 { return @@ -867,7 +868,7 @@ func gio_onKeyboardModifiers(data unsafe.Pointer, keyboard *C.struct_wl_keyboard if conn.xkb == nil { return } - conn.xkb.UpdateMask(uint32(depressed), uint32(latched), uint32(locked), uint32(group)) + conn.xkb.UpdateMask(uint32(depressed), uint32(latched), uint32(locked), uint32(group), uint32(group), uint32(group)) } //export gio_onKeyboardRepeatInfo @@ -1082,6 +1083,12 @@ func detectUIScale() float32 { func waylandConnect() error { c := new(wlConn) conn = c + xkb, err := xkb.New() + if err != nil { + c.destroy() + return fmt.Errorf("wayland: %v", err) + } + c.xkb = xkb c.disp = C.wl_display_connect(nil) if c.disp == nil { c.destroy() diff --git a/app/internal/window/os_x11.go b/app/internal/window/os_x11.go index d4d69873..2498a34d 100644 --- a/app/internal/window/os_x11.go +++ b/app/internal/window/os_x11.go @@ -5,38 +5,17 @@ package window /* -#cgo LDFLAGS: -lX11 +#cgo LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb #include #include #include #include #include #include +#include +#include +#include -void gio_x11_init_ime(Display *dpy, Window win, XIM *xim, XIC *xic) { - // adjust locale temporarily for XOpenIM - char *lc = setlocale(LC_CTYPE, NULL); - setlocale(LC_CTYPE, ""); - XSetLocaleModifiers(""); - - *xim = XOpenIM(dpy, 0, 0, 0); - if (!*xim) { - // fallback to internal input method - XSetLocaleModifiers("@im=none"); - *xim = XOpenIM(dpy, 0, 0, 0); - } - - // revert locale to prevent any unexpected side effects - setlocale(LC_CTYPE, lc); - - *xic = XCreateIC(*xim, - XNInputStyle, XIMPreeditNothing | XIMStatusNothing, - XNClientWindow, win, - XNFocusWindow, win, - NULL); - - XSetICFocus(*xic); -} */ import "C" import ( @@ -46,29 +25,29 @@ import ( "strconv" "sync" "time" - "unicode" - "unicode/utf8" "unsafe" "gioui.org/f32" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" + + "gioui.org/app/internal/xkb" syscall "golang.org/x/sys/unix" ) type x11Window struct { - w Callbacks - x *C.Display - xw C.Window + w Callbacks + x *C.Display + xkb *xkb.Context + xkbEventBase C.int + xw C.Window evDelWindow C.Atom stage system.Stage cfg config width int height int - xim C.XIM - xic C.XIC notify struct { read, write int } @@ -192,8 +171,10 @@ func (w *x11Window) destroy() { syscall.Close(w.notify.read) w.notify.read = 0 } - C.XDestroyIC(w.xic) - C.XCloseIM(w.xim) + if w.xkb != nil { + w.xkb.Destroy() + w.xkb = nil + } C.XDestroyWindow(w.x, w.xw) C.XCloseDisplay(w.x) } @@ -235,60 +216,22 @@ func (h *x11EventHandler) handleEvents() bool { continue } switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type { + case h.w.xkbEventBase: + xkbEvent := (*C.XkbAnyEvent)(unsafe.Pointer(xev)) + switch xkbEvent.xkb_type { + case C.XkbNewKeyboardNotify, C.XkbMapNotify: + if err := h.w.updateXkbKeymap(); err != nil { + panic(err) + } + case C.XkbStateNotify: + state := (*C.XkbStateNotifyEvent)(unsafe.Pointer(xev)) + h.w.xkb.UpdateMask(uint32(state.base_mods), uint32(state.latched_mods), uint32(state.locked_mods), + uint32(state.base_group), uint32(state.latched_group), uint32(state.locked_group)) + } case C.KeyPress: kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev)) - lookup: - // Save state then clear CTRL & Shift bits in order to have - // Xutf8LookupString return the unmodified key name in text[:l]. - // This addresses an issue on some non US keyboard layouts where - // CTRL-[0..9] do not behave consistently. - // - // Note that this enables sending a key.Event for key combinations - // like CTRL-SHIFT-/ on QWERTY layouts, but CTRL-? is completely - // masked. The same applies to AZERTY layouts where CTRL-SHIFT-É is - // available but not CTRL-2. - state := kevt.state - mods := x11KeyStateToModifiers(state) - if mods.Contain(key.ModCtrl) { - kevt.state &^= (C.uint(C.ControlMask) | C.uint(C.ShiftMask)) - } - l := int(C.Xutf8LookupString(w.xic, kevt, - (*C.char)(unsafe.Pointer(&h.text[0])), C.int(len(h.text)), - &h.keysym, &h.status)) - switch h.status { - case C.XBufferOverflow: - h.text = make([]byte, l) - goto lookup - case C.XLookupChars: - // Synthetic event from XIM. - w.w.Event(key.EditEvent{Text: string(h.text[:l])}) - case C.XLookupKeySym: - // Special keys. - if n, m, ok := x11ConvertKeysym(h.keysym, mods); ok { - w.w.Event(key.Event{Name: n, Modifiers: m}) - } - case C.XLookupBoth: - if n, m, ok := x11ConvertKeysym(h.keysym, mods); ok { - w.w.Event(key.Event{Name: n, Modifiers: m}) - } - // Do not send EditEvent for CTRL key combinations. - if mods.Contain(key.ModCtrl) { - break - } - // Report only printable runes. - str := h.text[:l] - for n := 0; n < len(str); { - r, s := utf8.DecodeRune(str) - if unicode.IsPrint(r) { - n += s - } else { - copy(str[n:], str[n+s:]) - str = str[:len(str)-s] - } - } - if len(str) > 0 { - w.w.Event(key.EditEvent{Text: string(str)}) - } + for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode)) { + w.w.Event(e) } case C.KeyRelease: case C.ButtonPress, C.ButtonRelease: @@ -369,98 +312,6 @@ func (h *x11EventHandler) handleEvents() bool { return redraw } -func x11KeyStateToModifiers(s C.uint) key.Modifiers { - var m key.Modifiers - if s&C.Mod1Mask != 0 { - m |= key.ModAlt - } - if s&C.Mod4Mask != 0 { - m |= key.ModSuper - } - if s&C.ControlMask != 0 { - m |= key.ModCtrl - } - if s&C.ShiftMask != 0 { - m |= key.ModShift - } - return m -} - -// x11ConvertKeysym returns the Gio special key that matches keysym s. For -// portability reasons, some keysyms might be translated and modifiers changed -// (like BackTab -> ModShift+Tab) -func x11ConvertKeysym(s C.KeySym, mods key.Modifiers) (string, key.Modifiers, bool) { - if '0' <= s && s <= '9' || 'A' <= s && s <= 'Z' { - return string(s), mods, true - } - if 'a' <= s && s <= 'z' { - return string(s - 0x20), mods, true - } - var n string - switch s { - case C.XK_Escape: - n = key.NameEscape - case C.XK_Left, C.XK_KP_Left: - n = key.NameLeftArrow - case C.XK_Right, C.XK_KP_Right: - n = key.NameRightArrow - case C.XK_Return: - n = key.NameReturn - case C.XK_KP_Enter: - n = key.NameEnter - case C.XK_Up, C.XK_KP_Up: - n = key.NameUpArrow - case C.XK_Down, C.XK_KP_Down: - n = key.NameDownArrow - case C.XK_Home, C.XK_KP_Home: - n = key.NameHome - case C.XK_End, C.XK_KP_End: - n = key.NameEnd - case C.XK_BackSpace: - n = key.NameDeleteBackward - case C.XK_Delete, C.XK_KP_Delete: - n = key.NameDeleteForward - case C.XK_Page_Up, C.XK_KP_Prior: - n = key.NamePageUp - case C.XK_Page_Down, C.XK_KP_Next: - n = key.NamePageDown - case C.XK_F1: - n = "F1" - case C.XK_F2: - n = "F2" - case C.XK_F3: - n = "F3" - case C.XK_F4: - n = "F4" - case C.XK_F5: - n = "F5" - case C.XK_F6: - n = "F6" - case C.XK_F7: - n = "F7" - case C.XK_F8: - n = "F8" - case C.XK_F9: - n = "F9" - case C.XK_F10: - n = "F10" - case C.XK_F11: - n = "F11" - case C.XK_F12: - n = "F12" - case C.XK_ISO_Left_Tab: - mods |= key.ModShift - fallthrough - case C.XK_Tab: - n = key.NameTab - case 0x20, C.XK_KP_Space: - n = "Space" - default: - return "", mods, false - } - return n, mods, true -} - var ( x11Threads sync.Once ) @@ -490,6 +341,22 @@ func newX11Window(gioWin Callbacks, opts *Options) error { if dpy == nil { return errors.New("x11: cannot connect to the X server") } + var major, minor C.int = C.XkbMajorVersion, C.XkbMinorVersion + var xkbEventBase C.int + if C.XkbQueryExtension(dpy, nil, &xkbEventBase, nil, &major, &minor) != C.True { + C.XCloseDisplay(dpy) + return errors.New("x11: XkbQueryExtension failed") + } + const bits = C.uint(C.XkbNewKeyboardNotifyMask | C.XkbMapNotifyMask | C.XkbStateNotifyMask) + if C.XkbSelectEvents(dpy, C.XkbUseCoreKbd, bits, bits) != C.True { + C.XCloseDisplay(dpy) + return errors.New("x11: XkbSelectEvents failed") + } + xkb, err := xkb.New() + if err != nil { + C.XCloseDisplay(dpy) + return fmt.Errorf("x11: %v", err) + } ppsp := x11DetectUIScale(dpy) cfg := config{pxPerDp: ppsp, pxPerSp: ppsp} @@ -506,23 +373,23 @@ func newX11Window(gioWin Callbacks, opts *Options) error { 0, 0, C.uint(cfg.Px(opts.Width)), C.uint(cfg.Px(opts.Height)), 0, C.CopyFromParent, C.InputOutput, nil, C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa) - var ( - xim C.XIM - xic C.XIC - ) - C.gio_x11_init_ime(dpy, win, &xim, &xic) w := &x11Window{ w: gioWin, x: dpy, xw: win, - width: cfg.Px(opts.Width), - height: cfg.Px(opts.Height), - cfg: cfg, - xim: xim, - xic: xic, + width: cfg.Px(opts.Width), + height: cfg.Px(opts.Height), + cfg: cfg, + xkb: xkb, + xkbEventBase: xkbEventBase, } w.notify.read = pipe[0] w.notify.write = pipe[1] + if err := w.updateXkbKeymap(); err != nil { + w.destroy() + return err + } + var hints C.XWMHints hints.input = C.True hints.flags = C.InputHint @@ -591,3 +458,27 @@ func x11DetectUIScale(dpy *C.Display) float32 { return scale } + +func (w *x11Window) updateXkbKeymap() error { + w.xkb.DestroyKeymapState() + ctx := (*C.struct_xkb_context)(unsafe.Pointer(w.xkb.Ctx)) + xcb := C.XGetXCBConnection(w.x) + if xcb == nil { + return errors.New("x11: XGetXCBConnection failed") + } + xkbDevID := C.xkb_x11_get_core_keyboard_device_id(xcb) + if xkbDevID == -1 { + return errors.New("x11: xkb_x11_get_core_keyboard_device_id failed") + } + keymap := C.xkb_x11_keymap_new_from_device(ctx, xcb, xkbDevID, C.XKB_KEYMAP_COMPILE_NO_FLAGS) + if keymap == nil { + return errors.New("x11: xkb_x11_keymap_new_from_device failed") + } + state := C.xkb_x11_state_new_from_device(keymap, xcb, xkbDevID) + if state == nil { + C.xkb_keymap_unref(keymap) + return errors.New("x11: xkb_x11_keymap_new_from_device failed") + } + w.xkb.SetKeymap(unsafe.Pointer(keymap), unsafe.Pointer(state)) + return nil +} diff --git a/app/internal/xkb/xkb_unix.go b/app/internal/xkb/xkb_unix.go index cc33be8c..23bd87c6 100644 --- a/app/internal/xkb/xkb_unix.go +++ b/app/internal/xkb/xkb_unix.go @@ -9,8 +9,8 @@ import ( "errors" "fmt" "os" - "unicode" "syscall" + "unicode" "unicode/utf8" "unsafe" @@ -30,7 +30,7 @@ import ( import "C" type Context struct { - ctx *C.struct_xkb_context + Ctx *C.struct_xkb_context keyMap *C.struct_xkb_keymap state *C.struct_xkb_state compTable *C.struct_xkb_compose_table @@ -54,25 +54,18 @@ func (x *Context) Destroy() { C.xkb_compose_table_unref(x.compTable) x.compTable = nil } - if x.state != nil { - C.xkb_state_unref(x.state) - x.state = nil - } - if x.keyMap != nil { - C.xkb_keymap_unref(x.keyMap) - x.keyMap = nil - } - if x.ctx != nil { - C.xkb_context_unref(x.ctx) - x.ctx = nil + x.DestroyKeymapState() + if x.Ctx != nil { + C.xkb_context_unref(x.Ctx) + x.Ctx = nil } } -func New(format int, fd int, size int) (*Context, error) { +func New() (*Context, error) { ctx := &Context{ - ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS), + Ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS), } - if ctx.ctx == nil { + if ctx.Ctx == nil { return nil, errors.New("newXKB: xkb_context_new failed") } locale := os.Getenv("LC_ALL") @@ -87,7 +80,7 @@ func New(format int, fd int, size int) (*Context, error) { } cloc := C.CString(locale) defer C.free(unsafe.Pointer(cloc)) - ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.ctx, cloc, C.XKB_COMPOSE_COMPILE_NO_FLAGS) + ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.Ctx, cloc, C.XKB_COMPOSE_COMPILE_NO_FLAGS) if ctx.compTable == nil { ctx.Destroy() return nil, errors.New("newXKB: xkb_compose_table_new_from_locale failed") @@ -97,27 +90,54 @@ func New(format int, fd int, size int) (*Context, error) { ctx.Destroy() return nil, errors.New("newXKB: xkb_compose_state_new failed") } - mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED) - if err != nil { - ctx.Destroy() - return nil, fmt.Errorf("newXKB: mmap of keymap failed: %v", err) - } - defer syscall.Munmap(mapData) - ctx.keyMap = C.xkb_keymap_new_from_buffer(ctx.ctx, (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1), C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS) - if ctx.keyMap == nil { - ctx.Destroy() - return nil, errors.New("newXKB: xkb_keymap_new_from_buffer failed") - } - ctx.state = C.xkb_state_new(ctx.keyMap) - if ctx.state == nil { - ctx.Destroy() - return nil, errors.New("newXKB: xkb_state_new failed") - } return ctx, nil } +func (x *Context) DestroyKeymapState() { + if x.state != nil { + C.xkb_state_unref(x.state) + x.state = nil + } + if x.keyMap != nil { + C.xkb_keymap_unref(x.keyMap) + x.keyMap = nil + } +} + +// SetKeymap sets the keymap and state. The context takes ownership of the +// keymap and state and frees them in Destroy. +func (x *Context) SetKeymap(xkbKeyMap, xkbState unsafe.Pointer) { + x.DestroyKeymapState() + x.keyMap = (*C.struct_xkb_keymap)(xkbKeyMap) + x.state = (*C.struct_xkb_state)(xkbState) +} + +func (x *Context) LoadKeymap(format int, fd int, size int) error { + x.DestroyKeymapState() + mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED) + if err != nil { + return fmt.Errorf("newXKB: mmap of keymap failed: %v", err) + } + defer syscall.Munmap(mapData) + keyMap := C.xkb_keymap_new_from_buffer(x.Ctx, (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1), C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS) + if keyMap == nil { + return errors.New("newXKB: xkb_keymap_new_from_buffer failed") + } + state := C.xkb_state_new(keyMap) + if state == nil { + C.xkb_keymap_unref(keyMap) + return errors.New("newXKB: xkb_state_new failed") + } + x.keyMap = keyMap + x.state = state + return nil +} + func (x *Context) DispatchKey(keyCode uint32) (events []event.Event) { - kc := mapXKBKeyCode(keyCode) + if x.state == nil { + return + } + kc := C.xkb_keycode_t(keyCode) if len(x.utf8Buf) == 0 { x.utf8Buf = make([]byte, 1) } @@ -186,18 +206,16 @@ func (x *Context) charsForKeycode(keyCode C.xkb_keycode_t) []byte { } func (x *Context) IsRepeatKey(keyCode uint32) bool { - kc := mapXKBKeyCode(keyCode) + kc := C.xkb_keycode_t(keyCode) return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1 } -func (x *Context) UpdateMask(depressed, latched, locked, group uint32) { - xkbGrp := C.xkb_layout_index_t(group) - C.xkb_state_update_mask(x.state, C.xkb_mod_mask_t(depressed), C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked), xkbGrp, xkbGrp, xkbGrp) -} - -func mapXKBKeyCode(keyCode uint32) C.xkb_keycode_t { - // According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode." - return C.xkb_keycode_t(keyCode + 8) +func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latchedGroup, lockedGroup uint32) { + if x.state == nil { + return + } + C.xkb_state_update_mask(x.state, C.xkb_mod_mask_t(depressed), C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked), + C.xkb_layout_index_t(depressedGroup), C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup)) } func convertKeysym(s C.xkb_keysym_t) (string, bool) {