Touchscreen keyboard
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Mon, 12 Apr 2021 08:55:44 +0000 (01:55 -0700)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Mon, 12 Apr 2021 08:55:44 +0000 (01:55 -0700)
src/kbd/Makefile [new file with mode: 0644]
src/kbd/bbkbd.c [new file with mode: 0644]
src/kbd/button.c [new file with mode: 0644]
src/kbd/button.h [new file with mode: 0644]
src/kbd/config.h [new file with mode: 0644]
src/kbd/drw.c [new file with mode: 0644]
src/kbd/drw.h [new file with mode: 0644]
src/kbd/keyboard.c [new file with mode: 0644]
src/kbd/keyboard.h [new file with mode: 0644]
src/kbd/util.c [new file with mode: 0644]
src/kbd/util.h [new file with mode: 0644]

diff --git a/src/kbd/Makefile b/src/kbd/Makefile
new file mode 100644 (file)
index 0000000..4a624fa
--- /dev/null
@@ -0,0 +1,32 @@
+NAME = bbkbd
+
+PKG_CONFIG = pkg-config
+PKGS = fontconfig freetype2 x11 xtst xft xinerama
+
+CDEFS = -D_DEFAULT_SOURCE -DXINERAMA
+CFLAGS += -I. `$(PKG_CONFIG) --cflags $(PKGS)` $(CDEFS)
+CFLAGS += -MD -MP -MT $@ -MF build/dep/$(@F).d
+CFLAGS += -Wall -Werror -g
+LDFLAGS += `$(PKG_CONFIG) --libs $(PKGS)`
+
+SRC = $(wildcard *.c)
+OBJ := $(patsubst %.c,build/%.o,$(SRC))
+
+all: $(NAME)
+
+build/%.o: %.c
+       $(CC) $(CFLAGS) -c -o $@ $<
+
+$(NAME): $(OBJ)
+       $(CC) -o $@ $(OBJ) $(LDFLAGS)
+
+tidy:
+       rm -f *~ \#*
+
+clean: tidy
+       rm -rf $(NAME) *.o build
+
+.PHONY: all clean tidy
+
+# Dependencies
+-include $(shell mkdir -p build/dep) $(wildcard build/dep/*)
diff --git a/src/kbd/bbkbd.c b/src/kbd/bbkbd.c
new file mode 100644 (file)
index 0000000..ee9ce34
--- /dev/null
@@ -0,0 +1,138 @@
+/******************************************************************************\
+
+                  This file is part of the Buildbotics firmware.
+
+         Copyright (c) 2015 - 2021, Buildbotics LLC, All rights reserved.
+
+          This Source describes Open Hardware and is licensed under the
+                                  CERN-OHL-S v2.
+
+          You may redistribute and modify this Source and make products
+     using it under the terms of the CERN-OHL-S v2 (https:/cern.ch/cern-ohl).
+            This Source is distributed WITHOUT ANY EXPRESS OR IMPLIED
+     WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS
+      FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable
+                                   conditions.
+
+                 Source location: https://github.com/buildbotics
+
+       As per CERN-OHL-S v2 section 4, should You produce hardware based on
+     these sources, You must maintain the Source Location clearly visible on
+     the external case of the CNC Controller or other product you make using
+                                   this Source.
+
+                 For more information, email info@buildbotics.com
+
+\******************************************************************************/
+
+#include "keyboard.h"
+#include "button.h"
+#include "drw.h"
+#include "util.h"
+#include "config.h"
+
+#include <signal.h>
+#include <locale.h>
+
+#define DEFAULT_FONT "DejaVu Sans:bold:size=22"
+
+static const char *font = DEFAULT_FONT;
+static int button_x = -60;
+static int button_y = 0;
+static bool running = true;
+
+
+void signaled(int sig) {
+  running = false;
+  print_dbg("Signal %d received\n", sig);
+}
+
+
+void usage(char *argv0, int ret) {
+  const char *usage =
+    "usage: %s [-hdb] [-f <font>] [-b <x> <y>]\n"
+    "Options:\n"
+    "  -h         - Print this help screen and exit\n"
+    "  -d         - Enable debug\n"
+    "  -f <font>  - Font string, default: " DEFAULT_FONT "\n"
+    "  -b <x> <y> - Button screen position.\n";
+
+  fprintf(ret ? stderr : stdout, usage, argv0);
+  exit(ret);
+}
+
+
+void parse_args(int argc, char *argv[]) {
+  for (int i = 1; argv[i]; i++) {
+    if (!strcmp(argv[i], "-h")) usage(argv[0], 0);
+    else if (!strcmp(argv[i], "-d")) debug = true;
+    else if (!strcmp(argv[i], "-f")) {
+      if (argc - 1 <= i) usage(argv[0], 1);
+      font = argv[++i];
+
+    } else if (!strcmp(argv[i], "-b")) {
+      if (argc - 2 <= i) usage(argv[0], 1);
+      button_x = atoi(argv[++i]);
+      button_y = atoi(argv[++i]);
+
+    } else {
+      fprintf(stderr, "Invalid argument: %s\n", argv[i]);
+      usage(argv[0], 1);
+    }
+  }
+}
+
+
+int main(int argc, char *argv[]) {
+  signal(SIGTERM, signaled);
+  signal(SIGINT, signaled);
+
+  parse_args(argc, argv);
+
+  // Check locale support
+  if (!setlocale(LC_CTYPE, "") || !XSupportsLocale())
+    fprintf(stderr, "warning: no locale support");
+
+  // Init
+  Display *dpy = XOpenDisplay(0);
+  if (!dpy) die("cannot open display");
+
+  int size = 60;
+  Button *btn = button_create(dpy, 0, button_x, button_y, size, size * 0.5,
+                              font);
+  Keyboard *kbd = keyboard_create(dpy, layers, font, colors);
+  btn->kbd = kbd;
+
+  // Event loop
+  while (running) {
+    struct timeval tv;
+    tv.tv_sec = 0;
+    tv.tv_usec = 100000; // 100ms
+
+    int xfd = ConnectionNumber(dpy);
+    fd_set fds;
+    FD_ZERO(&fds);
+    FD_SET(xfd, &fds);
+    int r = select(xfd + 1, &fds, 0, 0, &tv);
+
+    if (r == -1) break;
+
+    while (r && XPending(dpy)) {
+      XEvent ev;
+      XNextEvent(dpy, &ev);
+
+      if (ev.xany.window == kbd->win)
+        keyboard_event(kbd, &ev);
+
+      if (ev.xany.window == btn->win)
+        button_event(btn, &ev);
+    }
+  }
+
+  // Cleanup
+  button_destroy(btn);
+  keyboard_destroy(kbd);
+  XCloseDisplay(dpy);
+
+  return 0;
+}
diff --git a/src/kbd/button.c b/src/kbd/button.c
new file mode 100644 (file)
index 0000000..14940d7
--- /dev/null
@@ -0,0 +1,144 @@
+/******************************************************************************\
+
+                  This file is part of the Buildbotics firmware.
+
+         Copyright (c) 2015 - 2021, Buildbotics LLC, All rights reserved.
+
+          This Source describes Open Hardware and is licensed under the
+                                  CERN-OHL-S v2.
+
+          You may redistribute and modify this Source and make products
+     using it under the terms of the CERN-OHL-S v2 (https:/cern.ch/cern-ohl).
+            This Source is distributed WITHOUT ANY EXPRESS OR IMPLIED
+     WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS
+      FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable
+                                   conditions.
+
+                 Source location: https://github.com/buildbotics
+
+       As per CERN-OHL-S v2 section 4, should You produce hardware based on
+     these sources, You must maintain the Source Location clearly visible on
+     the external case of the CNC Controller or other product you make using
+                                   this Source.
+
+                 For more information, email info@buildbotics.com
+
+\******************************************************************************/
+
+#include "button.h"
+#include "util.h"
+
+#include <X11/Xatom.h>
+
+
+void button_draw(Button *btn) {
+  drw_rect(btn->drw, 0, 0, 100, 100, 1, 1);
+
+  const char *label = "⌨";
+  int h = btn->drw->fonts[0].xfont->height * 2;
+  int y = (btn->h - h) / 2;
+  int w = drw_fontset_getwidth(btn->drw, label);
+  int x = (btn->w - w) / 2;
+  drw_text(btn->drw, x, y, w, h, 0, label, 0);
+
+  drw_map(btn->drw, btn->win, 0, 0, 100, 100);
+}
+
+
+void button_event(Button *btn, XEvent *e) {
+  switch (e->type) {
+  case MotionNotify: {
+    int x = e->xmotion.x;
+    int y = e->xmotion.y;
+    btn->mouse_in = 0 <= x && x < btn->w && 0 <= y && y < btn->h;
+    break;
+  }
+
+  case ButtonPress: break;
+
+  case ButtonRelease:
+    if (e->xbutton.button == 1 && btn->mouse_in && btn->kbd)
+      keyboard_toggle(btn->kbd);
+    break;
+
+  case Expose: if (!e->xexpose.count) button_draw(btn); break;
+  }
+}
+
+
+Button *button_create(Display *dpy, Keyboard *kbd, int x, int y, int w, int h,
+                      const char *font) {
+  Button *btn = (Button *)calloc(1, sizeof(Button));
+  btn->kbd = kbd;
+
+  int screen = DefaultScreen(dpy);
+  Window root = RootWindow(dpy, screen);
+
+  // Dimensions
+  Dim dim = get_display_dims(dpy, screen);
+  x = x < 0 ? dim.width - w : 0;
+  y = y < 0 ? dim.height - h : 0;
+  btn->w = w;
+  btn->h = h;
+
+  // Create drawable
+  Drw *drw = btn->drw = drw_create(dpy, screen, root, w, h);
+
+  // Setup font
+  if (!drw_fontset_create(drw, &font, 1)) die("no fonts could be loaded");
+
+  // Init color scheme
+  const char *colors[] = {"#bbbbbb", "#132a33"};
+  btn->scheme = drw_scm_create(drw, colors, 2);
+  drw_setscheme(drw, btn->scheme);
+
+  XSetWindowAttributes wa;
+  wa.override_redirect = true;
+
+  btn->win = XCreateWindow
+    (dpy, root, x, y, w, h, 0, CopyFromParent, CopyFromParent,
+     CopyFromParent, CWOverrideRedirect | CWBorderPixel | CWBackingPixel, &wa);
+
+  // Enable window events
+  XSelectInput(dpy, btn->win, ButtonReleaseMask | ButtonPressMask |
+               ExposureMask | PointerMotionMask);
+
+  // Set window properties
+  XWMHints *wmHints = XAllocWMHints();
+  wmHints->input = false;
+  wmHints->flags = InputHint;
+
+  const char *name = "bbkbd-button";
+  XTextProperty str;
+  XStringListToTextProperty((char **)&name, 1, &str);
+
+  XClassHint *classHints = XAllocClassHint();
+  classHints->res_class = (char *)name;
+  classHints->res_name = (char *)name;
+
+  XSetWMProperties(dpy, btn->win, &str, &str, 0, 0, 0, wmHints, classHints);
+
+  XFree(classHints);
+  XFree(wmHints);
+  XFree(str.value);
+
+  // Set window type
+  Atom atom = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", false);
+  Atom type = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_UTILITY", false);
+  XChangeProperty(dpy, btn->win, atom, XA_ATOM, 32, PropModeReplace,
+                  (unsigned char *)&type, 1);
+
+  // Raise window to top of stack
+  XMapRaised(dpy, btn->win);
+
+  return btn;
+}
+
+
+void button_destroy(Button *btn) {
+  drw_sync(btn->drw);
+  drw_free(btn->drw);
+
+  free(btn->scheme);
+  free(btn);
+}
diff --git a/src/kbd/button.h b/src/kbd/button.h
new file mode 100644 (file)
index 0000000..1a8b669
--- /dev/null
@@ -0,0 +1,50 @@
+/******************************************************************************\
+
+                  This file is part of the Buildbotics firmware.
+
+         Copyright (c) 2015 - 2021, Buildbotics LLC, All rights reserved.
+
+          This Source describes Open Hardware and is licensed under the
+                                  CERN-OHL-S v2.
+
+          You may redistribute and modify this Source and make products
+     using it under the terms of the CERN-OHL-S v2 (https:/cern.ch/cern-ohl).
+            This Source is distributed WITHOUT ANY EXPRESS OR IMPLIED
+     WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS
+      FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable
+                                   conditions.
+
+                 Source location: https://github.com/buildbotics
+
+       As per CERN-OHL-S v2 section 4, should You produce hardware based on
+     these sources, You must maintain the Source Location clearly visible on
+     the external case of the CNC Controller or other product you make using
+                                   this Source.
+
+                 For more information, email info@buildbotics.com
+
+\******************************************************************************/
+
+#pragma once
+
+#include "keyboard.h"
+
+#include <stdbool.h>
+
+
+typedef struct {
+  Keyboard *kbd;
+  Window win;
+  Drw *drw;
+  Clr *scheme;
+
+  int w;
+  int h;
+  bool mouse_in;
+} Button;
+
+
+Button *button_create(Display *dpy, Keyboard *kbd, int x, int y, int w, int h,
+                      const char *font);
+void button_destroy(Button *btn);
+void button_event(Button *btn, XEvent *e);
diff --git a/src/kbd/config.h b/src/kbd/config.h
new file mode 100644 (file)
index 0000000..164235a
--- /dev/null
@@ -0,0 +1,231 @@
+/******************************************************************************\
+
+                  This file is part of the Buildbotics firmware.
+
+         Copyright (c) 2015 - 2021, Buildbotics LLC, All rights reserved.
+
+          This Source describes Open Hardware and is licensed under the
+                                  CERN-OHL-S v2.
+
+          You may redistribute and modify this Source and make products
+     using it under the terms of the CERN-OHL-S v2 (https:/cern.ch/cern-ohl).
+            This Source is distributed WITHOUT ANY EXPRESS OR IMPLIED
+     WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS
+      FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable
+                                   conditions.
+
+                 Source location: https://github.com/buildbotics
+
+       As per CERN-OHL-S v2 section 4, should You produce hardware based on
+     these sources, You must maintain the Source Location clearly visible on
+     the external case of the CNC Controller or other product you make using
+                                   this Source.
+
+                 For more information, email info@buildbotics.com
+
+\******************************************************************************/
+
+#pragma once
+
+#include "keyboard.h"
+
+
+static const char *colors[SchemeLast][2] = {
+  //                         fg         bg
+  [SchemeNorm]           = {"#bbbbbb", "#132a33"},
+  [SchemeNormABC]        = {"#ffffff", "#14313d"},
+  [SchemePress]          = {"#ffffff", "#259937"},
+  [SchemeHighlight]      = {"#58a7c6", "#005577"},
+};
+
+
+static Key _main[] = {
+  {"q", "Q", XK_q, 1},
+  {"w", "W", XK_w, 1},
+  {"e", "E", XK_e, 1},
+  {"r", "R", XK_r, 1},
+  {"t", "T", XK_t, 1},
+  {"y", "Y", XK_y, 1},
+  {"u", "U", XK_u, 1},
+  {"i", "I", XK_i, 1},
+  {"o", "O", XK_o, 1},
+  {"p", "P", XK_p, 1},
+  {"7", "&", XK_7, 1},
+  {"8", "*", XK_8, 1},
+  {"9", "(", XK_9, 1},
+  {"-", "_", XK_minus, 1},
+
+  {0}, // New row
+
+  {"a", "A", XK_a, 1},
+  {"s", "S", XK_s, 1},
+  {"d", "D", XK_d, 1},
+  {"f", "F", XK_f, 1},
+  {"g", "G", XK_g, 1},
+  {"h", "H", XK_h, 1},
+  {"j", "J", XK_j, 1},
+  {"k", "K", XK_k, 1},
+  {"l", "L", XK_l, 1},
+  {";", ":", XK_colon, 1},
+  {"4", "$", XK_4, 1},
+  {"5", "%", XK_5, 1},
+  {"6", "^", XK_6, 1},
+  {"=", "+", XK_equal, 1},
+
+  {0}, // New row
+
+  {"z", "Z", XK_z, 1},
+  {"x", "X", XK_x, 1},
+  {"c", "C", XK_c, 1},
+  {"v", "V", XK_v, 1},
+  {"b", "B", XK_b, 1},
+  {"n", "N", XK_n, 1},
+  {"m", "M", XK_m, 1},
+  {"Tab", 0, XK_Tab, 1},
+  {"⇍ Bksp", 0, XK_BackSpace, 2},
+  {"1", "!", XK_1, 1},
+  {"2", "@", XK_2, 1},
+  {"3", "#", XK_3, 1},
+  {"/", "?", XK_slash, 1},
+
+  {0}, // New row
+  {"⌨", 0, XK_Cancel, 1},
+  {"Shift", 0, XK_Shift_L, 1},
+  {"↓", 0, XK_Down, 1},
+  {"↑", 0, XK_Up, 1},
+  {"Space", 0, XK_space, 2},
+  {"Esc", 0, XK_Escape, 1},
+  {"Ctrl", 0, XK_Control_L, 1},
+  {"↲ Enter", 0, XK_Return, 2},
+  {"0", ")", XK_0, 1},
+  {",", "<", XK_comma, 1},
+  {".", ">", XK_period, 1},
+  {"\\", "|", XK_slash, 1},
+
+  {0}, {0} // End
+};
+
+
+static Key _alt[] = {
+  {0, 0, XK_Q, 1},
+  {0, 0, XK_W, 1},
+  {0, 0, XK_E, 1},
+  {0, 0, XK_R, 1},
+  {0, 0, XK_T, 1},
+  {0, 0, XK_Y, 1},
+  {0, 0, XK_U, 1},
+  {0, 0, XK_I, 1},
+  {0, 0, XK_O, 1},
+  {0, 0, XK_P, 1},
+  {"7", 0, XK_7, 1},
+  {"8", 0, XK_8, 1},
+  {"9", 0, XK_9, 1},
+  {"-", 0, XK_minus, 1},
+
+  {0}, // New row
+
+  {0, 0, XK_A, 1},
+  {0, 0, XK_S, 1},
+  {0, 0, XK_D, 1},
+  {0, 0, XK_F, 1},
+  {0, 0, XK_G, 1},
+  {0, 0, XK_H, 1},
+  {0, 0, XK_J, 1},
+  {0, 0, XK_K, 1},
+  {0, 0, XK_L, 1},
+  {";",":", XK_colon, 1},
+  {"4", 0, XK_4, 1},
+  {"5", 0, XK_5, 1},
+  {"6", 0, XK_6, 1},
+  {"+", 0, XK_plus, 1},
+
+  {0}, // New row
+
+  {0, 0, XK_Z, 1},
+  {0, 0, XK_X, 1},
+  {0, 0, XK_C, 1},
+  {0, 0, XK_V, 1},
+  {0, 0, XK_B, 1},
+  {0, 0, XK_N, 1},
+  {0, 0, XK_M, 1},
+  {"Tab", 0, XK_Tab, 1},
+  {"⇍ Bksp", 0, XK_BackSpace, 2},
+  {"1", 0, XK_1, 1},
+  {"2", 0, XK_2, 1},
+  {"3", 0, XK_3, 1},
+  {"/", 0, XK_slash, 1},
+
+  {0}, // New row
+  {"⌨", 0, XK_Cancel, 1},
+  {"Shift", 0, XK_Shift_L, 1},
+  {"↓", 0, XK_Down, 1},
+  {"↑", 0, XK_Up, 1},
+  {"Space", 0, XK_space, 2},
+  {"Esc", 0, XK_Escape, 1},
+  {"Ctrl", 0, XK_Control_L, 1},
+  {"↲ Enter", 0, XK_Return, 2},
+  {"0", 0, XK_0, 1},
+  {".", 0, XK_period, 1},
+  {"=", 0, XK_equal, 1},
+  {"*", 0, XK_asterisk, 1},
+
+  {0}, {0} // End
+};
+
+
+Key _symbols[] = {
+  {"1", "!", XK_1, 1},
+  {"2", "@", XK_2, 1},
+  {"3", "#", XK_3, 1},
+  {"4", "$", XK_4, 1},
+  {"5", "%", XK_5, 1},
+  {"6", "^", XK_6, 1},
+  {"7", "&", XK_7, 1},
+  {"8", "*", XK_8, 1},
+  {"9", "(", XK_9, 1},
+  {"0", ")", XK_0, 1},
+
+  {0}, // New row
+
+  {"'", "\"", XK_apostrophe, 1},
+  {"`", "~", XK_grave, 1},
+  {"-", "_", XK_minus, 1},
+  {"=", "+", XK_plus, 1},
+  {"[", "{", XK_bracketleft, 1},
+  {"]", "}", XK_bracketright, 1},
+  {",", "<", XK_comma, 1},
+  {".", ">", XK_period, 1},
+  {"/", "?", XK_slash, 1},
+  {"\\", "|", XK_backslash, 1},
+
+  {0}, // New row
+
+  {"", 0, XK_Shift_L|XK_bar, 1},
+  {"⇤", 0, XK_Home, 1},
+  {"←", 0, XK_Left, 1},
+  {"→", 0, XK_Right, 1},
+  {"⇥", 0, XK_End, 1},
+  {"⇊", 0, XK_Next, 1},
+  {"⇈", 0, XK_Prior, 1},
+  {"Tab", 0, XK_Tab, 1},
+  {"⇍ Bksp", 0, XK_BackSpace, 2},
+
+  {0}, // New row
+  {"⌨", 0, XK_Cancel, 1},
+  {"Shift", 0, XK_Shift_L, 1},
+  {"↓", 0, XK_Down, 1},
+  {"↑", 0, XK_Up, 1},
+  {"", 0, XK_space, 2},
+  {"Esc", 0, XK_Escape, 1},
+  {"Ctrl", 0, XK_Control_L, 1},
+  {"↲ Enter", 0, XK_Return, 2},
+
+  {0}, {0} // End
+};
+
+
+static Key *layers[] = {
+  _main,
+  _alt,
+  0
+};
diff --git a/src/kbd/drw.c b/src/kbd/drw.c
new file mode 100644 (file)
index 0000000..896326f
--- /dev/null
@@ -0,0 +1,407 @@
+/******************************************************************************\
+
+                  This file is part of the Buildbotics firmware.
+
+         Copyright (c) 2015 - 2021, Buildbotics LLC, All rights reserved.
+
+          This Source describes Open Hardware and is licensed under the
+                                  CERN-OHL-S v2.
+
+          You may redistribute and modify this Source and make products
+     using it under the terms of the CERN-OHL-S v2 (https:/cern.ch/cern-ohl).
+            This Source is distributed WITHOUT ANY EXPRESS OR IMPLIED
+     WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS
+      FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable
+                                   conditions.
+
+                 Source location: https://github.com/buildbotics
+
+       As per CERN-OHL-S v2 section 4, should You produce hardware based on
+     these sources, You must maintain the Source Location clearly visible on
+     the external case of the CNC Controller or other product you make using
+                                   this Source.
+
+                 For more information, email info@buildbotics.com
+
+\******************************************************************************/
+
+#include "drw.h"
+#include "util.h"
+
+#include <string.h>
+#include <stdint.h>
+
+
+#define UTF_INVALID 0xFFFD
+#define UTF_SIZ     4
+
+static const uint8_t utfbyte[] = {0x80,    0, 0xC0, 0xE0, 0xF0};
+static const uint8_t utfmask[] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
+static const long utfmin[] = {       0,    0,  0x80,  0x800, 0x10000};
+static const long utfmax[] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
+
+
+static long utf8decodebyte(const char c, size_t *i) {
+  for (*i = 0; *i < (UTF_SIZ + 1); ++(*i))
+    if (((uint8_t)c & utfmask[*i]) == utfbyte[*i])
+      return (uint8_t)c & ~utfmask[*i];
+
+  return 0;
+}
+
+
+static size_t utf8validate(long *u, size_t i) {
+  if (!BETWEEN(*u, utfmin[i], utfmax[i]) || BETWEEN(*u, 0xD800, 0xDFFF))
+    *u = UTF_INVALID;
+
+  for (i = 1; *u > utfmax[i]; ++i) continue;
+
+  return i;
+}
+
+
+static size_t utf8decode(const char *c, long *u, size_t clen) {
+  size_t i, j, len, type;
+
+  *u = UTF_INVALID;
+  if (!clen) return 0;
+  long udecoded = utf8decodebyte(c[0], &len);
+
+  if (!BETWEEN(len, 1, UTF_SIZ))
+    return 1;
+
+  for (i = 1, j = 1; i < clen && j < len; ++i, ++j) {
+    udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type);
+    if (type) return j;
+  }
+
+  if (j < len) return 0;
+  *u = udecoded;
+  utf8validate(u, len);
+
+  return len;
+}
+
+
+Drw *drw_create(Display *dpy, int screen, Window root, unsigned w, unsigned h) {
+  Drw *drw = calloc(1, sizeof(Drw));
+
+  drw->dpy = dpy;
+  drw->screen = screen;
+  drw->root = root;
+  drw->w = w;
+  drw->h = h;
+  drw->drawable = XCreatePixmap(dpy, root, w, h, DefaultDepth(dpy, screen));
+  drw->gc = XCreateGC(dpy, root, 0, 0);
+  XSetLineAttributes(dpy, drw->gc, 1, LineSolid, CapButt, JoinMiter);
+
+  return drw;
+}
+
+
+void drw_resize(Drw *drw, unsigned w, unsigned h) {
+  if (!drw) return;
+
+  drw->w = w;
+  drw->h = h;
+  if (drw->drawable) XFreePixmap(drw->dpy, drw->drawable);
+  drw->drawable = XCreatePixmap(drw->dpy, drw->root, w, h, DefaultDepth(drw->dpy, drw->screen));
+}
+
+
+void drw_free(Drw *drw) {
+  XFreePixmap(drw->dpy, drw->drawable);
+  XFreeGC(drw->dpy, drw->gc);
+  drw_fontset_free(drw->fonts);
+  free(drw);
+}
+
+
+/// This function is an implementation detail. Library users should use
+/// drw_fontset_create instead.
+static Fnt *xfont_create(Drw *drw, const char *fontname,
+                         FcPattern *fontpattern) {
+  XftFont *xfont = 0;
+  FcPattern *pattern = 0;
+
+  if (fontname) {
+    // Using the pattern found at font->xfont->pattern does not yield the
+    // same substitution results as using the pattern returned by
+    // FcNameParse; using the latter results in the desired fallback
+    // behaviour whereas the former just results in missing-character
+    // rectangles being drawn, at least with some fonts.
+    if (!(xfont = XftFontOpenName(drw->dpy, drw->screen, fontname))) {
+      fprintf(stderr, "error, cannot load font from name: '%s'\n", fontname);
+      return 0;
+    }
+
+    if (!(pattern = FcNameParse((FcChar8 *) fontname))) {
+      fprintf(stderr, "error, cannot parse font name to pattern: '%s'\n",
+              fontname);
+      XftFontClose(drw->dpy, xfont);
+      return 0;
+    }
+
+  } else if (fontpattern) {
+    if (!(xfont = XftFontOpenPattern(drw->dpy, fontpattern))) {
+      fprintf(stderr, "error, cannot load font from pattern.\n");
+      return 0;
+    }
+
+  } else die("no font specified.");
+
+  // Do not allow using color fonts. This is a workaround for a BadLength
+  // error from Xft with color glyphs. Modelled on the Xterm workaround. See
+  // https://bugzilla.redhat.com/show_bug.cgi?id=1498269
+  // https://lists.suckless.org/dev/1701/30932.html
+  // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=916349
+  // and lots more all over the internet.
+  FcBool iscol;
+  if (FcPatternGetBool(xfont->pattern, FC_COLOR, 0, &iscol) == FcResultMatch &&
+      iscol) {
+    XftFontClose(drw->dpy, xfont);
+    return 0;
+  }
+
+  Fnt *font = calloc(1, sizeof(Fnt));
+  font->xfont = xfont;
+  font->pattern = pattern;
+  font->h = xfont->ascent + xfont->descent;
+  font->dpy = drw->dpy;
+
+  return font;
+}
+
+
+static void xfont_free(Fnt *font) {
+  if (!font) return;
+  if (font->pattern) FcPatternDestroy(font->pattern);
+  XftFontClose(font->dpy, font->xfont);
+  free(font);
+}
+
+
+Fnt *drw_fontset_create(Drw *drw, const char *fonts[], size_t count) {
+  Fnt *cur, *ret = 0;
+
+  if (!drw || !fonts) return 0;
+
+  for (size_t i = 1; i <= count; i++)
+    if ((cur = xfont_create(drw, fonts[count - i], 0))) {
+      cur->next = ret;
+      ret = cur;
+    }
+
+  return drw->fonts = ret;
+}
+
+
+void drw_fontset_free(Fnt *font) {
+  if (font) {
+    drw_fontset_free(font->next);
+    xfont_free(font);
+  }
+}
+
+
+void drw_clr_create(Drw *drw, Clr *dest, const char *clrname) {
+  if (!drw || !dest || !clrname) return;
+
+  if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen),
+                         DefaultColormap(drw->dpy, drw->screen),
+                         clrname, dest))
+    die("error, cannot allocate color '%s'", clrname);
+}
+
+
+/// Wrapper to create color schemes. The caller has to call free(3) on the
+/// returned color scheme when done using it.
+Clr *drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount) {
+  Clr *ret;
+
+  // need at least two colors for a scheme
+  if (!drw || !clrnames || clrcount < 2 ||
+      !(ret = calloc(clrcount, sizeof(XftColor))))
+    die("error, cannot create color scheme (drw=%d) (clrcount=%d)", drw,
+        clrcount);
+
+  for (size_t i = 0; i < clrcount; i++)
+    drw_clr_create(drw, &ret[i], clrnames[i]);
+
+  return ret;
+}
+
+
+void drw_setfontset(Drw *drw, Fnt *set) {if (drw) drw->fonts = set;}
+void drw_setscheme(Drw *drw, Clr *scm) {if (drw) drw->scheme = scm;}
+
+
+void drw_rect(Drw *drw, int x, int y, unsigned w, unsigned h, int filled,
+              int invert) {
+  if (!drw || !drw->scheme) return;
+
+  XSetForeground(drw->dpy, drw->gc,
+                 invert ? drw->scheme[ColBg].pixel : drw->scheme[ColFg].pixel);
+
+  if (filled) XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
+  else XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1);
+}
+
+
+int drw_text(Drw *drw, int x, int y, unsigned w, unsigned h, unsigned lpad,
+             const char *text, int invert) {
+  char buf[1024];
+  int ty;
+  XftDraw *d = 0;
+  Fnt *curfont, *nextfont;
+  size_t i, len;
+  int utf8strlen, utf8charlen, render = x || y || w || h;
+  long utf8codepoint = 0;
+  const char *utf8str;
+  FcCharSet *fccharset;
+  FcPattern *fcpattern;
+  FcPattern *match;
+  XftResult result;
+  int charexists = 0;
+
+  if (!drw || (render && !drw->scheme) || !text || !drw->fonts) return 0;
+
+  if (!render) w = ~w;
+  else {
+    XSetForeground(drw->dpy, drw->gc, drw->scheme[invert ? ColFg : ColBg].pixel);
+    XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
+    d = XftDrawCreate(drw->dpy, drw->drawable,
+                      DefaultVisual(drw->dpy, drw->screen),
+                      DefaultColormap(drw->dpy, drw->screen));
+    x += lpad;
+    w -= lpad;
+  }
+
+  Fnt *usedfont = drw->fonts;
+
+  while (1) {
+    utf8strlen = 0;
+    utf8str = text;
+    nextfont = 0;
+
+    while (*text) {
+      utf8charlen = utf8decode(text, &utf8codepoint, UTF_SIZ);
+      for (curfont = drw->fonts; curfont; curfont = curfont->next) {
+        charexists =
+          charexists || XftCharExists(drw->dpy, curfont->xfont, utf8codepoint);
+        if (charexists) {
+          if (curfont == usedfont) {
+            utf8strlen += utf8charlen;
+            text += utf8charlen;
+
+          } else nextfont = curfont;
+          break;
+        }
+      }
+
+      if (!charexists || nextfont) break;
+      else charexists = 0;
+    }
+
+    if (utf8strlen) {
+      unsigned ew = 0;
+      drw_font_getexts(usedfont, utf8str, utf8strlen, &ew, 0);
+
+      // shorten text if necessary
+      for (len = MIN(utf8strlen, sizeof(buf) - 1); len && ew > w; len--)
+        drw_font_getexts(usedfont, utf8str, len, &ew, 0);
+
+      if (len) {
+        memcpy(buf, utf8str, len);
+        buf[len] = '\0';
+        if (len < utf8strlen)
+          for (i = len; i && i > len - 3; buf[--i] = '.') continue;
+
+        if (render) {
+          ty = y + (h - usedfont->h) / 2 + usedfont->xfont->ascent;
+          XftDrawStringUtf8(d, &drw->scheme[invert ? ColBg : ColFg],
+                            usedfont->xfont, x, ty, (XftChar8 *)buf, len);
+        }
+
+        x += ew;
+        w -= ew;
+      }
+    }
+
+    if (!*text) break;
+    else if (nextfont) {
+      charexists = 0;
+      usedfont = nextfont;
+
+    } else {
+      // Regardless of whether or not a fallback font is found, the
+      // character must be drawn.
+      charexists = 1;
+
+      fccharset = FcCharSetCreate();
+      FcCharSetAddChar(fccharset, utf8codepoint);
+
+      if (!drw->fonts->pattern)
+        // Refer to the comment in xfont_create for more information.
+        die("the first font in the cache must be loaded from a font string.");
+
+      fcpattern = FcPatternDuplicate(drw->fonts->pattern);
+      FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset);
+      FcPatternAddBool(fcpattern, FC_SCALABLE, FcTrue);
+      FcPatternAddBool(fcpattern, FC_COLOR, FcFalse);
+
+      FcConfigSubstitute(0, fcpattern, FcMatchPattern);
+      FcDefaultSubstitute(fcpattern);
+      match = XftFontMatch(drw->dpy, drw->screen, fcpattern, &result);
+
+      FcCharSetDestroy(fccharset);
+      FcPatternDestroy(fcpattern);
+
+      if (match) {
+        usedfont = xfont_create(drw, 0, match);
+
+        if (usedfont &&
+            XftCharExists(drw->dpy, usedfont->xfont, utf8codepoint)) {
+          for (curfont = drw->fonts; curfont->next; curfont = curfont->next)
+            continue;
+          curfont->next = usedfont;
+
+        } else {
+          xfont_free(usedfont);
+          usedfont = drw->fonts;
+        }
+      }
+    }
+  }
+
+  if (d) XftDrawDestroy(d);
+
+  return x + (render ? w : 0);
+}
+
+
+void drw_map(Drw *drw, Window win, int x, int y, unsigned w, unsigned h) {
+  if (!drw) return;
+  XCopyArea(drw->dpy, drw->drawable, win, drw->gc, x, y, w, h, x, y);
+}
+
+
+void drw_sync(Drw *drw) {XSync(drw->dpy, False);}
+
+
+unsigned drw_fontset_getwidth(Drw *drw, const char *text) {
+  if (!drw || !drw->fonts || !text) return 0;
+  return drw_text(drw, 0, 0, 0, 0, 0, text, 0);
+}
+
+
+void drw_font_getexts(Fnt *font, const char *text, unsigned len, unsigned *w,
+                      unsigned *h) {
+  XGlyphInfo ext;
+
+  if (!font || !text) return;
+
+  XftTextExtentsUtf8(font->dpy, font->xfont, (XftChar8 *)text, len, &ext);
+  if (w) *w = ext.xOff;
+  if (h) *h = font->h;
+}
diff --git a/src/kbd/drw.h b/src/kbd/drw.h
new file mode 100644 (file)
index 0000000..56ebb2f
--- /dev/null
@@ -0,0 +1,90 @@
+/******************************************************************************\
+
+                  This file is part of the Buildbotics firmware.
+
+         Copyright (c) 2015 - 2021, Buildbotics LLC, All rights reserved.
+
+          This Source describes Open Hardware and is licensed under the
+                                  CERN-OHL-S v2.
+
+          You may redistribute and modify this Source and make products
+     using it under the terms of the CERN-OHL-S v2 (https:/cern.ch/cern-ohl).
+            This Source is distributed WITHOUT ANY EXPRESS OR IMPLIED
+     WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS
+      FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable
+                                   conditions.
+
+                 Source location: https://github.com/buildbotics
+
+       As per CERN-OHL-S v2 section 4, should You produce hardware based on
+     these sources, You must maintain the Source Location clearly visible on
+     the external case of the CNC Controller or other product you make using
+                                   this Source.
+
+                 For more information, email info@buildbotics.com
+
+\******************************************************************************/
+
+#pragma once
+
+#include <X11/Xlib.h>
+#include <X11/Xft/Xft.h>
+
+
+#define MAX(A, B)               ((A) > (B) ? (A) : (B))
+#define MIN(A, B)               ((A) < (B) ? (A) : (B))
+#define BETWEEN(X, A, B)        ((A) <= (X) && (X) <= (B))
+
+
+typedef struct Fnt {
+  Display *dpy;
+  unsigned h;
+  XftFont *xfont;
+  FcPattern *pattern;
+  struct Fnt *next;
+} Fnt;
+
+enum {ColFg, ColBg}; // Clr scheme index
+typedef XftColor Clr;
+
+typedef struct {
+  unsigned w, h;
+  Display *dpy;
+  int screen;
+  Window root;
+  Drawable drawable;
+  GC gc;
+  Clr *scheme;
+  Fnt *fonts;
+} Drw;
+
+
+// Drawable abstraction
+Drw *drw_create(Display *dpy, int screen, Window win, unsigned w, unsigned h);
+void drw_resize(Drw *drw, unsigned w, unsigned h);
+void drw_free(Drw *drw);
+
+// Fnt abstraction
+Fnt *drw_fontset_create(Drw *drw, const char *fonts[], size_t fontcount);
+void drw_fontset_free(Fnt *set);
+unsigned drw_fontset_getwidth(Drw *drw, const char *text);
+void drw_font_getexts(Fnt *font, const char *text, unsigned len, unsigned *w,
+                      unsigned *h);
+
+// Colorscheme abstraction
+void drw_clr_create(Drw *drw, Clr *dest, const char *clrname);
+Clr *drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount);
+
+// Drawing context manipulation
+void drw_setfontset(Drw *drw, Fnt *set);
+void drw_setscheme(Drw *drw, Clr *scm);
+
+// Drawing functions
+void drw_rect(Drw *drw, int x, int y, unsigned w, unsigned h, int filled,
+              int invert);
+int drw_text(Drw *drw, int x, int y, unsigned w, unsigned h, unsigned lpad,
+             const char *text, int invert);
+
+// Map functions
+void drw_map(Drw *drw, Window win, int x, int y, unsigned w, unsigned h);
+void drw_sync(Drw *drw);
diff --git a/src/kbd/keyboard.c b/src/kbd/keyboard.c
new file mode 100644 (file)
index 0000000..147aadc
--- /dev/null
@@ -0,0 +1,396 @@
+/******************************************************************************\
+
+                  This file is part of the Buildbotics firmware.
+
+         Copyright (c) 2015 - 2021, Buildbotics LLC, All rights reserved.
+
+          This Source describes Open Hardware and is licensed under the
+                                  CERN-OHL-S v2.
+
+          You may redistribute and modify this Source and make products
+     using it under the terms of the CERN-OHL-S v2 (https:/cern.ch/cern-ohl).
+            This Source is distributed WITHOUT ANY EXPRESS OR IMPLIED
+     WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS
+      FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable
+                                   conditions.
+
+                 Source location: https://github.com/buildbotics
+
+       As per CERN-OHL-S v2 section 4, should You produce hardware based on
+     these sources, You must maintain the Source Location clearly visible on
+     the external case of the CNC Controller or other product you make using
+                                   this Source.
+
+                 For more information, email info@buildbotics.com
+
+\******************************************************************************/
+
+#include "keyboard.h"
+
+#include <X11/Xatom.h>
+
+#include <signal.h>
+
+
+static int create_window(Display *dpy, int root, const char *name, Dim dim,
+                         int x, int y, bool override, unsigned long fg,
+                         unsigned long bg) {
+  XSetWindowAttributes wa;
+  wa.override_redirect = override;
+  wa.border_pixel = fg;
+  wa.background_pixel = bg;
+
+  int win = XCreateWindow
+    (dpy, root, x, y, dim.width, dim.height, 0, CopyFromParent, CopyFromParent,
+     CopyFromParent, CWOverrideRedirect | CWBorderPixel | CWBackingPixel, &wa);
+
+  // Enable window events
+  XSelectInput(dpy, win, StructureNotifyMask | ButtonReleaseMask |
+               ButtonPressMask | ExposureMask | PointerMotionMask |
+               LeaveWindowMask);
+
+  // Set window properties
+  XWMHints *wmHints = XAllocWMHints();
+  wmHints->input = false;
+  wmHints->flags = InputHint;
+
+  XTextProperty str;
+  XStringListToTextProperty((char **)&name, 1, &str);
+
+  XClassHint *classHints = XAllocClassHint();
+  classHints->res_class = (char *)name;
+  classHints->res_name = (char *)name;
+
+  XSetWMProperties(dpy, win, &str, &str, 0, 0, 0, wmHints, classHints);
+
+  XFree(classHints);
+  XFree(wmHints);
+  XFree(str.value);
+
+  // Set window type
+  Atom atom = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", false);
+  Atom type = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DOCK", false);
+  XChangeProperty(dpy, win, atom, XA_ATOM, 32, PropModeReplace,
+                  (unsigned char *)&type, 1);
+
+  return win;
+}
+
+
+static bool is_modifier(Key *k) {return k && IsModifierKey(k->keysym);}
+
+
+static int key_scheme(Keyboard *kbd, Key *k) {
+  if (k->pressed) return SchemePress;
+  if (k == kbd->focus) return SchemeHighlight;
+  if (k->keysym == XK_Return || (XK_a <= k->keysym && k->keysym <= XK_z) ||
+      (XK_Cyrillic_io <= k->keysym && k->keysym <= XK_Cyrillic_hardsign))
+    return SchemeNormABC;
+
+  return SchemeNorm;
+}
+
+
+Key *keyboard_find_key(Keyboard *kbd, int x, int y) {
+  Key *keys = kbd->keys;
+
+  for (int i = 0; i < kbd->nkeys; i++)
+    if (keys[i].keysym && keys[i].x < x && x < keys[i].x + keys[i].w &&
+        keys[i].y < y && y < keys[i].y + keys[i].h)
+      return &keys[i];
+
+  return 0;
+}
+
+
+void keyboard_draw_key(Keyboard *kbd, Key *k) {
+  Drw *drw = kbd->drw;
+
+  drw_setscheme(drw, kbd->scheme[key_scheme(kbd, k)]);
+  drw_rect(drw, k->x, k->y, k->w, k->h, 1, 1);
+
+  const char *label = k->label;
+  if (!label) label = XKeysymToString(k->keysym);
+  if (kbd->shifted && k->label2) label = k->label2;
+
+  int h = drw->fonts[0].xfont->height * 2;
+  int y = k->y + (k->h - h) / 2;
+  int w = drw_fontset_getwidth(drw, label);
+  int x = k->x + (k->w - w) / 2;
+  drw_text(drw, x, y, w, h, 0, label, 0);
+
+  drw_map(drw, kbd->win, k->x, k->y, k->w, k->h);
+}
+
+
+void keyboard_draw(Keyboard *kbd) {
+  for (int i = 0; i < kbd->nkeys; i++)
+    if (kbd->keys[i].keysym)
+      keyboard_draw_key(kbd, &kbd->keys[i]);
+}
+
+
+void keyboard_update(Keyboard *kbd) {
+  int y = 0;
+  int r = kbd->nrows;
+  int h = (kbd->dim.height - 1) / r;
+  Key *keys = kbd->keys;
+
+  for (int i = 0; i < kbd->nkeys; i++, r--) {
+    int base = 0;
+
+    for (int j = i; j < kbd->nkeys && keys[j].keysym; j++)
+      base += keys[j].width;
+
+    for (int x = 0; i < kbd->nkeys && keys[i].keysym; i++) {
+      keys[i].x = x;
+      keys[i].y = y;
+      keys[i].w = keys[i].width * (kbd->dim.width - 1) / base;
+      keys[i].h = r == 1 ? kbd->dim.height - y - 1 : h;
+      x += keys[i].w;
+    }
+
+    if (base) keys[i - 1].w = kbd->dim.width - 1 - keys[i - 1].x;
+    y += h;
+  }
+
+  keyboard_draw(kbd);
+}
+
+
+void keyboard_init_layer(Keyboard *kbd) {
+  Key *layer = kbd->layers[kbd->layer];
+
+  // Count keys
+  kbd->nkeys = 0;
+
+  for (int i = 0; ; i++) {
+    if (0 < i && !layer[i].keysym && !layer[i - 1].keysym) {
+      kbd->nkeys--;
+      break;
+    }
+
+    kbd->nkeys++;
+  }
+
+  kbd->keys = calloc(1, sizeof(Key) * kbd->nkeys);
+  memcpy(kbd->keys, layer, sizeof(Key) * kbd->nkeys);
+
+  // Count rows
+  kbd->nrows = 1;
+
+  for (int i = 0; i < kbd->nkeys; i++)
+    if (!kbd->keys[i].keysym) {
+      kbd->nrows++;
+
+      if (i && !kbd->keys[i - 1].keysym) {
+        kbd->nrows--;
+        break;
+      }
+    }
+}
+
+
+void keyboard_next_layer(Keyboard *kbd) {
+  if (!kbd->layers[++kbd->layer]) kbd->layer = 0;
+
+  print_dbg("Cycling to layer %d\n", kbd->layer);
+
+  keyboard_init_layer(kbd);
+  keyboard_update(kbd);
+}
+
+
+void keyboard_press_key(Keyboard *kbd, Key *k) {
+  if (k->pressed) return;
+
+  if (k->keysym == XK_Shift_L || k->keysym == XK_Shift_R) {
+    kbd->shifted = true;
+    keyboard_draw(kbd);
+  }
+
+  simulate_key(kbd->drw->dpy, k->modifier, true);
+  simulate_key(kbd->drw->dpy, k->keysym, true);
+  k->pressed = true;
+  keyboard_draw_key(kbd, k);
+}
+
+
+void keyboard_unpress_key(Keyboard *kbd, Key *k) {
+  if (!k->pressed) return;
+
+  if (k->keysym == XK_Shift_L || k->keysym == XK_Shift_R) {
+    kbd->shifted = false;
+    keyboard_draw(kbd);
+  }
+
+  simulate_key(kbd->drw->dpy, k->keysym, false);
+  simulate_key(kbd->drw->dpy, k->modifier, false);
+  k->pressed = false;
+  keyboard_draw_key(kbd, k);
+}
+
+
+void keyboard_unpress_all(Keyboard *kbd) {
+  for (int i = 0; i < kbd->nkeys; i++)
+    keyboard_unpress_key(kbd, &kbd->keys[i]);
+}
+
+
+void keyboard_mouse_motion(Keyboard *kbd, int x, int y) {
+  Key *k = keyboard_find_key(kbd, x, y);
+  Key *focus = kbd->focus;
+
+  if (k == focus) return;
+  kbd->focus = k;
+
+  if (focus && !is_modifier(focus))
+    keyboard_unpress_key(kbd, focus);
+
+  if (k && kbd->is_pressing && !is_modifier(k))
+    keyboard_press_key(kbd, k);
+
+  if (k) keyboard_draw_key(kbd, k);
+  if (focus) keyboard_draw_key(kbd, focus);
+}
+
+
+void keyboard_mouse_press(Keyboard *kbd, int x, int y) {
+  kbd->is_pressing = true;
+
+  Key *k = keyboard_find_key(kbd, x, y);
+  if (k) {
+    if (is_modifier(k) && k->pressed) keyboard_unpress_key(kbd, k);
+    else keyboard_press_key(kbd, k);
+  }
+}
+
+
+void keyboard_mouse_release(Keyboard *kbd, int x, int y) {
+  kbd->is_pressing = false;
+
+  Key *k = keyboard_find_key(kbd, x, y);
+  if (k) {
+    switch (k->keysym) {
+    case XK_Cancel: keyboard_next_layer(kbd); break;
+    case XK_Break: raise(SIGINT); break;
+    default: break;
+    }
+
+    if (!is_modifier(k)) keyboard_unpress_all(kbd);
+  }
+}
+
+
+void keyboard_resize(Keyboard *kbd, int width, int height) {
+  if (width == kbd->dim.width && height == kbd->dim.height) return;
+
+  kbd->dim.width = width;
+  kbd->dim.height = height;
+  drw_resize(kbd->drw, width, height);
+  keyboard_update(kbd);
+}
+
+
+void keyboard_event(Keyboard *kbd, XEvent *e) {
+  switch (e->type) {
+  case LeaveNotify: keyboard_mouse_motion(kbd, -1, -1); break;
+
+  case MotionNotify:
+    keyboard_mouse_motion(kbd, e->xmotion.x, e->xmotion.y);
+    break;
+
+  case ButtonPress:
+    if (e->xbutton.button == 1)
+      keyboard_mouse_press(kbd, e->xbutton.x, e->xbutton.y);
+    break;
+
+  case ButtonRelease:
+    if (e->xbutton.button == 1)
+      keyboard_mouse_release(kbd, e->xbutton.x, e->xbutton.y);
+    break;
+
+  case ConfigureNotify:
+    keyboard_resize(kbd, e->xconfigure.width, e->xconfigure.height);
+    break;
+
+  case Expose:
+    if (!e->xexpose.count) keyboard_draw(kbd);
+    break;
+  }
+}
+
+
+void keyboard_toggle(Keyboard *kbd) {
+  kbd->visible = !kbd->visible;
+
+  if (kbd->visible) XMapRaised(kbd->drw->dpy, kbd->win);
+  else {
+    XUnmapWindow(kbd->drw->dpy, kbd->win);
+    keyboard_unpress_all(kbd);
+  }
+}
+
+
+Keyboard *keyboard_create(Display *dpy, Key **layers, const char *font,
+                          const char *colors[SchemeLast][2]) {
+  Keyboard *kbd = calloc(1, sizeof(Keyboard));
+  kbd->layers = layers;
+
+  // Init screen
+  int screen = DefaultScreen(dpy);
+  Window root = RootWindow(dpy, screen);
+
+  // Get display size
+  Dim dim = get_display_dims(dpy, screen);
+
+  // Init keyboard layer
+  keyboard_init_layer(kbd); // Computes kbd->nrows
+  kbd->dim.width = dim.width;
+  kbd->dim.height = dim.height * kbd->nrows / 18;
+
+  // Create drawable
+  Drw *drw = kbd->drw =
+    drw_create(dpy, screen, root, kbd->dim.width, kbd->dim.height);
+
+  // Setup fonts
+  if (!drw_fontset_create(drw, &font, 1)) die("no fonts could be loaded");
+
+  // Init color schemes
+  for (int i = 0; i < SchemeLast; i++)
+    kbd->scheme[i] = drw_scm_create(drw, colors[i], 2);
+
+  drw_setscheme(drw, kbd->scheme[SchemeNorm]);
+
+  // Create window
+  int y = dim.height - kbd->dim.height;
+  Clr *clr = kbd->scheme[SchemeNorm];
+  kbd->win = create_window(dpy, root, "bbkbd", kbd->dim, 0, y, false,
+                           clr[ColFg].pixel, clr[ColBg].pixel);
+
+  // Init keyboard
+  keyboard_update(kbd);
+
+  return kbd;
+}
+
+
+void keyboard_destroy(Keyboard *kbd) {
+  Display *dpy = kbd->drw->dpy;
+
+  keyboard_unpress_all(kbd);
+
+  drw_sync(kbd->drw);
+  drw_free(kbd->drw);
+
+  for (int i = 0; i < SchemeLast; i++)
+    free(kbd->scheme[i]);
+
+  XSync(dpy, false);
+  XDestroyWindow(dpy, kbd->win);
+  XSync(dpy, false);
+  XSetInputFocus(dpy, PointerRoot, RevertToPointerRoot, CurrentTime);
+
+  free(kbd->keys);
+  free(kbd);
+}
diff --git a/src/kbd/keyboard.h b/src/kbd/keyboard.h
new file mode 100644 (file)
index 0000000..826db84
--- /dev/null
@@ -0,0 +1,74 @@
+/******************************************************************************\
+
+                  This file is part of the Buildbotics firmware.
+
+         Copyright (c) 2015 - 2021, Buildbotics LLC, All rights reserved.
+
+          This Source describes Open Hardware and is licensed under the
+                                  CERN-OHL-S v2.
+
+          You may redistribute and modify this Source and make products
+     using it under the terms of the CERN-OHL-S v2 (https:/cern.ch/cern-ohl).
+            This Source is distributed WITHOUT ANY EXPRESS OR IMPLIED
+     WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS
+      FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable
+                                   conditions.
+
+                 Source location: https://github.com/buildbotics
+
+       As per CERN-OHL-S v2 section 4, should You produce hardware based on
+     these sources, You must maintain the Source Location clearly visible on
+     the external case of the CNC Controller or other product you make using
+                                   this Source.
+
+                 For more information, email info@buildbotics.com
+
+\******************************************************************************/
+
+#pragma once
+
+#include "drw.h"
+#include "util.h"
+
+#include <stdbool.h>
+
+
+enum {
+  SchemeNorm, SchemeNormABC, SchemePress, SchemeHighlight, SchemeLast
+};
+
+typedef struct {
+  char *label;
+  char *label2;
+  KeySym keysym;
+  unsigned width;
+  KeySym modifier;
+  int x, y, w, h;
+  bool pressed;
+} Key;
+
+typedef struct {
+  Window win;
+  Drw *drw;
+  Dim dim;
+  int layer;
+  int nrows;
+  int nkeys;
+  bool is_pressing;
+  bool shifted;
+  bool visible;
+
+  Key *focus;
+  Key **layers;
+  Key *keys;
+  char *font;
+  Clr *scheme[SchemeLast];
+} Keyboard;
+
+
+void keyboard_destroy(Keyboard *kbd);
+Keyboard *keyboard_create(Display *dpy, Key **layers, const char *font,
+                          const char *colors[SchemeLast][2]);
+
+void keyboard_event(Keyboard *kbd, XEvent *e);
+void keyboard_toggle(Keyboard *kbd);
diff --git a/src/kbd/util.c b/src/kbd/util.c
new file mode 100644 (file)
index 0000000..6690367
--- /dev/null
@@ -0,0 +1,145 @@
+/******************************************************************************\
+
+                  This file is part of the Buildbotics firmware.
+
+         Copyright (c) 2015 - 2021, Buildbotics LLC, All rights reserved.
+
+          This Source describes Open Hardware and is licensed under the
+                                  CERN-OHL-S v2.
+
+          You may redistribute and modify this Source and make products
+     using it under the terms of the CERN-OHL-S v2 (https:/cern.ch/cern-ohl).
+            This Source is distributed WITHOUT ANY EXPRESS OR IMPLIED
+     WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS
+      FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable
+                                   conditions.
+
+                 Source location: https://github.com/buildbotics
+
+       As per CERN-OHL-S v2 section 4, should You produce hardware based on
+     these sources, You must maintain the Source Location clearly visible on
+     the external case of the CNC Controller or other product you make using
+                                   this Source.
+
+                 For more information, email info@buildbotics.com
+
+\******************************************************************************/
+
+#include "util.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/extensions/XTest.h>
+
+#ifdef XINERAMA
+#include <X11/extensions/Xinerama.h>
+#endif
+
+
+bool debug = false;
+
+
+void die(const char *fmt, ...) {
+  va_list ap;
+
+  va_start(ap, fmt);
+  vfprintf(stderr, fmt, ap);
+  va_end(ap);
+
+  if (fmt[0] && fmt[strlen(fmt) - 1] == ':') {
+    fputc(' ', stderr);
+    perror(NULL);
+
+  } else fputc('\n', stderr);
+
+  exit(1);
+}
+
+
+void print_dbg(const char *fmt, ...) {
+  if (!debug) return;
+
+  va_list ap;
+  va_start(ap, fmt);
+  vfprintf(stderr, fmt, ap);
+  va_end(ap);
+  fflush(stderr);
+}
+
+
+
+int find_unused_keycode(Display *dpy) {
+  // Derived from:
+  // https://stackoverflow.com/questions/44313966/
+  //   c-xtest-emitting-key-presses-for-every-unicode-character
+
+  int keycode_low, keycode_high;
+  XDisplayKeycodes(dpy, &keycode_low, &keycode_high);
+
+  int keysyms_per_keycode = 0;
+  KeySym *keysyms =
+    XGetKeyboardMapping(dpy, keycode_low, keycode_high - keycode_low,
+                        &keysyms_per_keycode);
+
+  for (int i = keycode_low; i <= keycode_high; i++) {
+    bool key_is_empty = true;
+
+    for (int j = 0; j < keysyms_per_keycode; j++) {
+      int symindex = (i - keycode_low) * keysyms_per_keycode + j;
+      if (keysyms[symindex]) key_is_empty = false;
+      else break;
+    }
+
+    if (key_is_empty) {
+      XFree(keysyms);
+      return i;
+    }
+  }
+
+  XFree(keysyms);
+  return 1;
+}
+
+
+void simulate_key(Display *dpy, KeySym keysym, bool press) {
+  if (!keysym) return;
+
+  KeyCode code = XKeysymToKeycode(dpy, keysym);
+
+  if (!code) {
+    static int tmp_keycode = 0;
+    if (!tmp_keycode) tmp_keycode = find_unused_keycode(dpy);
+
+    code = tmp_keycode;
+    XChangeKeyboardMapping(dpy, tmp_keycode, 1, &keysym, 1);
+    XSync(dpy, false);
+  }
+
+  XTestFakeKeyEvent(dpy, code, press, 0);
+}
+
+
+Dim get_display_dims(Display *dpy, int screen) {
+  Dim dim;
+
+#ifdef XINERAMA
+  if (XineramaIsActive(dpy)) {
+    int i = 0;
+    XineramaScreenInfo *info = XineramaQueryScreens(dpy, &i);
+    dim.width  = info[0].width;
+    dim.height = info[0].height;
+    XFree(info);
+    return dim;
+  }
+#endif
+
+  dim.width  = DisplayWidth(dpy, screen);
+  dim.height = DisplayHeight(dpy, screen);
+
+  return dim;
+}
diff --git a/src/kbd/util.h b/src/kbd/util.h
new file mode 100644 (file)
index 0000000..202a149
--- /dev/null
@@ -0,0 +1,44 @@
+/******************************************************************************\
+
+                  This file is part of the Buildbotics firmware.
+
+         Copyright (c) 2015 - 2021, Buildbotics LLC, All rights reserved.
+
+          This Source describes Open Hardware and is licensed under the
+                                  CERN-OHL-S v2.
+
+          You may redistribute and modify this Source and make products
+     using it under the terms of the CERN-OHL-S v2 (https:/cern.ch/cern-ohl).
+            This Source is distributed WITHOUT ANY EXPRESS OR IMPLIED
+     WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS
+      FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable
+                                   conditions.
+
+                 Source location: https://github.com/buildbotics
+
+       As per CERN-OHL-S v2 section 4, should You produce hardware based on
+     these sources, You must maintain the Source Location clearly visible on
+     the external case of the CNC Controller or other product you make using
+                                   this Source.
+
+                 For more information, email info@buildbotics.com
+
+\******************************************************************************/
+
+#pragma once
+
+#include <X11/Xlib.h>
+
+#include <stdbool.h>
+
+extern bool debug;
+
+typedef struct {
+  int width;
+  int height;
+} Dim;
+
+void die(const char *fmt, ...);
+void print_dbg(const char *fmt, ...);
+void simulate_key(Display *dpy, KeySym keysym, bool press);
+Dim get_display_dims(Display *dpy, int screen);