Add a keyboard for text fields on the desktop

Fixes #7: SteamVR's keyboard never came up for the desktop's apps, and
opening it for our panels doesn't work well on the Frame (it's Steam's own
panel, mounted in the dashboard's scene, it follows the laser between
panels, and it takes the controllers over to SteamVR's laser).

- KWin starts input/ft-textinput as the desktop's input method. It tells
  the input relay when a text field gains or loses focus, and the relay
  asks ft-screens to open or close the keyboard. The session drops the
  QT_IM_MODULE=xim and GTK_IM_MODULE=xim that the gamescope session sets,
  or Qt and GTK apps never report text fields.
- The keyboard is ft-screens' own panel (screens/keyboard.cpp): a US laptop
  layout, typed with a controller's laser or the 3D mouse. It opens 0.7 m
  in front of you, below your eyes and facing you. It has a grab bar to
  move it, a Close key, latching Shift, Ctrl and Alt, and repeat on a held
  key. It's drawn into shared DMA-BUFs, so it doesn't flicker. Its keys
  reach the focused screen as key presses, so every app takes them.
- It steps aside while the Steam menu or Steam's own keyboard is up and
  comes back after. A layout reset closes it, and it doesn't open without
  a head pose.
- Frametop Input Settings has a Keyboard page: open it for every text
  field, only while no keyboard is connected (the default), only from a
  mapped button (the new Open/close keyboard action, for mice and
  controllers), or never. A switch keeps it open until you close it.
- The pointer helper treats every frametop.* overlay as a real panel. The
  keyboard's shared texture reports 0x0 like SteamVR's scene-graph
  controls, and the helper had given it their wide catch radius.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-09-30 13:41:47 -06:00
1 parent 3aea571496
commit 5714978e65
15 files changed
+1047 -33

No files matched your search

+5 -1
View File
@@ -5,20 +5,24 @@
# vr.cpp needs OpenVR's IVRIPCResourceManagerClient (ImportDmabuf), which the header
# shipped with SteamVR on the Frame predates, so the build uses the public header from
# Valve's openvr repo (pinned; the Frame's runtime supports its interface versions).
# keyboard.cpp draws its key labels with stb_truetype (public domain, one header, pinned).
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
"$root/scripts/sync.sh" >/dev/null
exec "$root/scripts/frame.sh" -C screens 'set -e; mkdir -p build/include
openvr=v2.15.6
[ -f build/include/openvr-$openvr ] || { curl -fsSL "https://raw.githubusercontent.com/ValveSoftware/openvr/$openvr/headers/openvr.h" -o build/include/openvr.h && touch build/include/openvr-$openvr; }
stb=2c980bb59875b0d32144a71867fbdebb2f77cd20
[ -f build/include/stb-$stb ] || { curl -fsSL "https://raw.githubusercontent.com/nothings/stb/$stb/stb_truetype.h" -o build/include/stb_truetype.h && touch build/include/stb-$stb; }
gcc -std=c11 -O2 -Wall -Wno-unused-parameter -c -o build/compositor.o compositor.c \
$(pkg-config --cflags wlroots-0.20 wayland-server xkbcommon libdrm pixman-1)
cxx="g++ -std=c++17 -O2 -Wall -Wno-missing-field-initializers -Ibuild/include $(pkg-config --cflags egl glesv2 gbm libdrm)"
$cxx -c -o build/vr.o vr.cpp
$cxx -c -o build/keyboard.o keyboard.cpp
$cxx -c -o build/handcut.o handcut.cpp
$cxx -c -o build/handtest.o handtest.cpp
vrlibs="$(pkg-config --libs egl glesv2 gbm) -L/opt/steamvr/bin/linuxarm64 -lopenvr_api -Wl,-rpath,/opt/steamvr/bin/linuxarm64"
g++ -o build/ft-screens build/compositor.o build/vr.o build/handcut.o \
g++ -o build/ft-screens build/compositor.o build/vr.o build/keyboard.o build/handcut.o \
$(pkg-config --libs wlroots-0.20 wayland-server xkbcommon) $vrlibs
g++ -o build/ft-handtest build/handtest.o build/handcut.o $vrlibs
echo "built build/ft-screens build/ft-handtest"'
+93 -9
View File
@@ -102,6 +102,10 @@ struct server {
int relay_fd; // unbound, so the relay can't reply into our control socket
uint32_t relay_sent; // when the relay last heard from us (ms)
unsigned ticks;
// Our keyboard ("vrkeyboard" commands from the input relay; see keyboard_command).
int kb_screen; // the screen it's open for, -1 closed
bool kb_auto; // opened for a focused text field (not by a button)
unsigned kb_close_at; // ticks: close it then (a text field lost focus), 0 not
};
static uint32_t now_ms(void) {
@@ -237,12 +241,24 @@ static void new_decoration(struct wl_listener *l, void *data) {
// ---------------------------------------------------------------- input from the panels
static void panel_key(struct server *s, uint32_t code, bool pressed);
static void handle_vr_event(const struct ft_event *e, void *data) {
struct server *s = data;
if (e->type == FT_QUIT) {
wl_display_terminate(s->display);
return;
}
if (e->type == FT_KEY) {
panel_key(s, e->key, e->pressed);
return;
}
if (e->type == FT_KEYBOARD_CLOSED) {
if (s->kb_screen >= 0) wlr_log(WLR_INFO, "keyboard closed");
s->kb_screen = -1;
s->kb_close_at = 0;
return;
}
if (e->screen < 0 || e->screen >= MAX_SCREENS || !s->screens[e->screen]) return;
struct screen *sc = s->screens[e->screen];
struct wlr_surface *surface = sc->toplevel->base->surface;
@@ -313,6 +329,13 @@ static int tick(void *data) {
struct server *s = data;
ft_vr_poll(handle_vr_event, s);
if (++s->ticks % 9 == 0) keys_update(s);
if (s->kb_close_at && s->ticks >= s->kb_close_at) {
s->kb_close_at = 0;
if (s->kb_screen >= 0) {
ft_vr_keyboard_hide();
s->kb_screen = -1;
}
}
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
for (int i = 0; i < MAX_SCREENS; ++i) {
@@ -344,6 +367,17 @@ static bool key_held(const struct wlr_keyboard *kb, uint32_t code) {
return false;
}
// One key to the focused screen, through the seat's keyboard so its xkb state and
// modifiers stay right.
static void send_key(struct server *s, uint32_t code, int pressed) {
struct wlr_keyboard_key_event ev = {
.time_msec = now_ms(), .keycode = code, .update_state = true,
.state = pressed ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED};
wlr_keyboard_notify_key(&s->keyboard, &ev);
wlr_seat_keyboard_notify_modifiers(s->seat, &s->keyboard.modifiers);
wlr_seat_keyboard_notify_key(s->seat, ev.time_msec, code, ev.state);
}
// Keys from the input relay (physical keyboards): "key <evdev code> <1 press|0 release>".
// They go to the screen KWin has keyboard focus on (the last one clicked), while typing
// goes to the desktop (keys_update). The release of a key the desktop got the press for
@@ -355,20 +389,67 @@ static void handle_key(struct server *s, uint32_t code, int value, char *reply,
if (!s->seat->keyboard_state.focused_surface) return (void)snprintf(reply, size, "ok no focus");
if (!s->keys_desktop) return (void)snprintf(reply, size, "ok typing goes to Steam");
}
struct wlr_keyboard_key_event ev = {
.time_msec = now_ms(), .keycode = code, .update_state = true,
.state = value ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED};
wlr_keyboard_notify_key(&s->keyboard, &ev); // keeps the xkb state and modifiers
wlr_seat_keyboard_notify_modifiers(s->seat, &s->keyboard.modifiers);
wlr_seat_keyboard_notify_key(s->seat, ev.time_msec, code, ev.state);
send_key(s, code, value);
snprintf(reply, size, "ok");
}
// Our keyboard (keyboard.cpp). The input relay sends "vrkeyboard show" when a text field
// on the desktop gets focus (and its setting allows), "vrkeyboard hide" when it loses it,
// and "vrkeyboard toggle" for a mapped button; ft-layout sends "vrkeyboard close" when it
// resets the layout. It opens for the screen KWin has keyboard focus on. A hide closes only a keyboard a text field opened, and waits a moment, so
// moving between text fields doesn't close and reopen it.
static int focused_screen(struct server *s) {
struct wlr_surface *focus = s->seat->keyboard_state.focused_surface;
for (int i = 0; i < MAX_SCREENS; ++i)
if (s->screens[i] && s->screens[i]->toplevel->base->surface == focus) return i;
if (s->pointer_focus) return s->pointer_focus->index;
for (int i = 0; i < MAX_SCREENS; ++i)
if (s->screens[i]) return i;
return -1;
}
static void keyboard_command(struct server *s, const char *what, char *reply, int size) {
const bool open = s->kb_screen >= 0;
if (strcmp(what, "hide") == 0) {
if (open && s->kb_auto && !s->kb_close_at) s->kb_close_at = s->ticks + 30; // ~1/3 s
return (void)snprintf(reply, size, "ok");
}
if (strcmp(what, "close") == 0) { // however it opened, now (ft-layout apply: a reset)
if (open) wlr_log(WLR_INFO, "keyboard closed (reset)");
ft_vr_keyboard_hide();
s->kb_screen = -1;
s->kb_close_at = 0;
return (void)snprintf(reply, size, "ok");
}
const bool toggle = strcmp(what, "toggle") == 0;
if (!toggle && strcmp(what, "show") != 0) return (void)snprintf(reply, size, "error show|hide|toggle|close");
s->kb_close_at = 0;
if (open && toggle) {
ft_vr_keyboard_hide();
s->kb_screen = -1;
return (void)snprintf(reply, size, "ok closed");
}
if (open) return (void)snprintf(reply, size, "ok open");
if (!ft_vr_screens_shown()) return (void)snprintf(reply, size, "ok screens hidden");
const int screen = focused_screen(s);
if (screen < 0 || !ft_vr_keyboard_show(screen)) return (void)snprintf(reply, size, "error not shown");
wlr_log(WLR_INFO, "keyboard open for screen %d (%s)", screen + 1, toggle ? "button" : "text field");
s->kb_screen = screen;
s->kb_auto = !toggle;
snprintf(reply, size, "ok opened");
}
// A key from our keyboard, for the focused screen. Its release always goes through, so
// no key stays held.
static void panel_key(struct server *s, uint32_t code, bool pressed) {
if (pressed ? s->seat->keyboard_state.focused_surface != NULL : key_held(&s->keyboard, code))
send_key(s, code, pressed);
}
// Control socket: abstract datagram @ft_screens. Here: "size <screen> <w> <h>" (a new
// resolution, live), "scale <screen> <s>" (KWin's scale for it, from ft-layout),
// "key <code> <value>", and "click <overlay key>" (from the pointer
// helper: a mouse click landed on that panel, "-" for none); the rest is in vr.cpp
// (ft_vr_command).
// "key <code> <value>", "vrkeyboard show|hide|toggle|close" (our keyboard, from the input
// relay), and "click <overlay key>" (from the pointer helper: a mouse click landed on
// that panel, "-" for none); the rest is in vr.cpp (ft_vr_command).
static int control_readable(int fd, uint32_t mask, void *data) {
struct server *s = data;
char buf[512], reply[2048];
@@ -402,6 +483,8 @@ static int control_readable(int fd, uint32_t mask, void *data) {
handle_key(s, code, value, reply, sizeof reply);
len = sizeof from;
continue; // no reply: keys are fire-and-forget
} else if (strncmp(buf, "vrkeyboard ", 11) == 0) {
keyboard_command(s, buf + 11, reply, sizeof reply);
} else if (strncmp(buf, "click ", 6) == 0) {
// A click on another panel takes typing to Steam. Our own panels (the screens,
// their controls) leave it: a click on a screen arrives as a panel event.
@@ -467,6 +550,7 @@ static bool setup_dmabuf(struct server *s) {
int main(int argc, char **argv) {
struct server s = {0};
for (int i = 0; i < MAX_SCREENS; ++i) s.scale[i] = 1;
s.kb_screen = -1;
const char *socket_name = "ft-screens-0";
char **command = NULL;
for (int i = 1; i < argc; ++i) {
+544
View File
@@ -0,0 +1,544 @@
// Frametop's keyboard: the panel ft-screens shows for a text field on the desktop (see
// ft_vr_keyboard_show in vr.cpp). A US laptop layout in five rows, with Esc where Caps Lock
// would be and a Close key by the arrows.
// - The keys send linux key codes, which ft-screens types into the focused screen through
// its seat, so they mean what they'd mean on a real keyboard. A key goes down on the
// press and up on the release, so KWin repeats a held key.
// - Shift, Ctrl and Alt latch: tap one and it's held for the next key; tap it again to let
// go. The labels follow Shift.
// - A grab bar along the top edge moves it, like a screen's bar: press it with any laser
// (or the 3D mouse) and the keyboard follows that device rigidly until the release.
// - The panel is drawn on the CPU: rounded keys, labels from Noto Sans (the container's,
// found with fc-match) rasterized with stb_truetype, and arrows drawn as triangles
// (Noto Sans has no arrow glyphs). It's drawn again when the key under a pointer, a
// pressed key, or a latch changes, into the next of three shared buffers (linear
// DMA-BUFs SteamVR imported once), and the panel switches to it. SetOverlayRaw, which
// uploads a new texture each time, left the panel blank for a moment on every change;
// it's only the fallback if the buffers can't be made.
#include "keyboard.h"
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"
#include <drm_fourcc.h>
#include <fcntl.h>
#include <gbm.h>
#include <linux/input-event-codes.h>
#include <unistd.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <map>
#include <string>
#include <vector>
namespace keyboard {
namespace {
constexpr int kW = 1280, kH = 470; // texture pixels
constexpr int kGrip = 40; // the grab bar's band along the top
constexpr double kWidthMetres = 0.7; // the panel's width in the room
constexpr int kMargin = 12, kGap = 7; // around the keys, between keys (pixels)
constexpr int kRows = 5, kColumns = 15; // row width in key units
constexpr uint32_t kHide = 0; // the Close key's code
struct KeyDef {
uint32_t code;
const char *label, *shifted; // shifted: nullptr = the same
double units; // width
};
// One row after another, 15 units each.
const std::vector<std::vector<KeyDef>> kLayout = {
{{KEY_GRAVE, "`", "~", 1}, {KEY_1, "1", "!", 1}, {KEY_2, "2", "@", 1}, {KEY_3, "3", "#", 1},
{KEY_4, "4", "$", 1}, {KEY_5, "5", "%", 1}, {KEY_6, "6", "^", 1}, {KEY_7, "7", "&", 1},
{KEY_8, "8", "*", 1}, {KEY_9, "9", "(", 1}, {KEY_0, "0", ")", 1}, {KEY_MINUS, "-", "_", 1},
{KEY_EQUAL, "=", "+", 1}, {KEY_BACKSPACE, "Backspace", nullptr, 2}},
{{KEY_TAB, "Tab", nullptr, 1.5}, {KEY_Q, "q", "Q", 1}, {KEY_W, "w", "W", 1}, {KEY_E, "e", "E", 1},
{KEY_R, "r", "R", 1}, {KEY_T, "t", "T", 1}, {KEY_Y, "y", "Y", 1}, {KEY_U, "u", "U", 1},
{KEY_I, "i", "I", 1}, {KEY_O, "o", "O", 1}, {KEY_P, "p", "P", 1}, {KEY_LEFTBRACE, "[", "{", 1},
{KEY_RIGHTBRACE, "]", "}", 1}, {KEY_BACKSLASH, "\\", "|", 1.5}},
{{KEY_ESC, "Esc", nullptr, 1.75}, {KEY_A, "a", "A", 1}, {KEY_S, "s", "S", 1}, {KEY_D, "d", "D", 1},
{KEY_F, "f", "F", 1}, {KEY_G, "g", "G", 1}, {KEY_H, "h", "H", 1}, {KEY_J, "j", "J", 1},
{KEY_K, "k", "K", 1}, {KEY_L, "l", "L", 1}, {KEY_SEMICOLON, ";", ":", 1},
{KEY_APOSTROPHE, "'", "\"", 1}, {KEY_ENTER, "Enter", nullptr, 2.25}},
{{KEY_LEFTSHIFT, "Shift", nullptr, 1.25}, {KEY_Z, "z", "Z", 1}, {KEY_X, "x", "X", 1},
{KEY_C, "c", "C", 1}, {KEY_V, "v", "V", 1}, {KEY_B, "b", "B", 1}, {KEY_N, "n", "N", 1},
{KEY_M, "m", "M", 1}, {KEY_COMMA, ",", "<", 1}, {KEY_DOT, ".", ">", 1}, {KEY_SLASH, "/", "?", 1},
{KEY_RIGHTSHIFT, "Shift", nullptr, 1.75}, {KEY_UP, "", nullptr, 1}, {KEY_DELETE, "Del", nullptr, 1}},
{{KEY_LEFTCTRL, "Ctrl", nullptr, 1.5}, {KEY_LEFTALT, "Alt", nullptr, 1.5}, {KEY_SPACE, "", nullptr, 7},
{kHide, "\u00d7 Close", nullptr, 2}, {KEY_LEFT, "", nullptr, 1}, {KEY_DOWN, "", nullptr, 1},
{KEY_RIGHT, "", nullptr, 1}},
};
struct Key {
KeyDef def;
int x0, y0, x1, y1; // texture pixels, top left origin
};
enum Mod { Shift, Ctrl, Alt, kMods };
const uint32_t kModCode[kMods] = {KEY_LEFTSHIFT, KEY_LEFTCTRL, KEY_LEFTALT};
int ModOf(uint32_t code) {
switch (code) {
case KEY_LEFTSHIFT:
case KEY_RIGHTSHIFT: return Shift;
case KEY_LEFTCTRL: return Ctrl;
case KEY_LEFTALT: return Alt;
default: return -1;
}
}
vr::VROverlayHandle_t g_overlay = vr::k_ulOverlayHandleInvalid;
bool g_shown = false;
std::vector<Key> g_keys;
std::vector<uint8_t> g_px;
int g_hover = -1; // key under a pointer
bool g_hoverGrip = false; // the grab bar is under a pointer
vr::TrackedDeviceIndex_t g_dragDevice = vr::k_unTrackedDeviceIndexInvalid; // carrying it
vr::HmdMatrix34_t g_dragRel{}; // device -> panel, while carried
vr::HmdMatrix34_t g_pose{}; // where it is
int g_down = -1; // key held down
bool g_latched[kMods] = {}; // Shift, Ctrl, Alt held for the next key
bool g_dirty = true; // draw again
// ---------------------------------------------------------------- labels
stbtt_fontinfo g_font;
std::vector<unsigned char> g_fontData;
bool g_fontOk = false;
struct Glyph {
std::vector<unsigned char> bitmap;
int w = 0, h = 0, xoff = 0, yoff = 0, advance = 0;
};
std::map<std::pair<uint32_t, int>, Glyph> g_glyphs; // (codepoint, pixel size)
void LoadFont() {
std::string path;
if (FILE *p = popen("fc-match -f '%{file}' 'Noto Sans' 2>/dev/null", "r")) {
char buf[512];
if (std::fgets(buf, sizeof buf, p)) path = buf;
pclose(p);
}
if (path.empty()) path = "/usr/share/fonts/google-noto-vf/NotoSans[wght].ttf";
if (FILE *f = std::fopen(path.c_str(), "rb")) {
std::fseek(f, 0, SEEK_END);
g_fontData.resize(size_t(std::ftell(f)));
std::fseek(f, 0, SEEK_SET);
g_fontOk = std::fread(g_fontData.data(), 1, g_fontData.size(), f) == g_fontData.size() &&
stbtt_InitFont(&g_font, g_fontData.data(), stbtt_GetFontOffsetForIndex(g_fontData.data(), 0));
std::fclose(f);
}
if (!g_fontOk) std::printf("keyboard: no font (%s); the keys have no labels\n", path.c_str());
}
const Glyph &GetGlyph(uint32_t cp, int size) {
auto [it, fresh] = g_glyphs.try_emplace({cp, size});
Glyph &g = it->second;
if (fresh) {
const float scale = stbtt_ScaleForPixelHeight(&g_font, float(size));
unsigned char *b = stbtt_GetCodepointBitmap(&g_font, 0, scale, int(cp), &g.w, &g.h, &g.xoff, &g.yoff);
if (b) g.bitmap.assign(b, b + size_t(g.w) * g.h), stbtt_FreeBitmap(b, nullptr);
int adv, lsb;
stbtt_GetCodepointHMetrics(&g_font, int(cp), &adv, &lsb);
g.advance = int(std::lround(adv * scale));
}
return g;
}
std::vector<uint32_t> Codepoints(const char *s) {
std::vector<uint32_t> out;
for (const unsigned char *p = (const unsigned char *)s; *p;) {
uint32_t c = *p++;
int more = c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 0;
if (more) c &= 0x3Fu >> more;
for (; more && (*p & 0xC0) == 0x80; --more) c = c << 6 | (*p++ & 0x3F);
out.push_back(c);
}
return out;
}
// Text centred on (cx, cy), white at `alpha`.
void DrawText(const char *text, int size, int cx, int cy, uint8_t alpha) {
if (!g_fontOk || !*text) return;
const auto cps = Codepoints(text);
int width = 0;
for (uint32_t cp : cps) width += GetGlyph(cp, size).advance;
int ascent, descent, gap;
stbtt_GetFontVMetrics(&g_font, &ascent, &descent, &gap);
const float scale = stbtt_ScaleForPixelHeight(&g_font, float(size));
int x = cx - width / 2;
const int baseline = cy + int(std::lround((ascent + descent) * scale / 2));
for (uint32_t cp : cps) {
const Glyph &g = GetGlyph(cp, size);
for (int gy = 0; gy < g.h; ++gy)
for (int gx = 0; gx < g.w; ++gx) {
const int px = x + g.xoff + gx, py = baseline + g.yoff + gy;
if (px < 0 || py < 0 || px >= kW || py >= kH) continue;
const double a = g.bitmap[size_t(gy) * g.w + gx] / 255.0 * alpha / 255.0;
uint8_t *p = &g_px[(size_t(py) * kW + px) * 4];
for (int c = 0; c < 3; ++c) p[c] = uint8_t(std::lround(p[c] + (255 - p[c]) * a));
}
x += g.advance;
}
}
// ---------------------------------------------------------------- drawing
// A white triangle pointing along (dx, dy) (one of them 0), centred on (cx, cy), `size` across.
void DrawArrow(int cx, int cy, int size, int dx, int dy, uint8_t alpha) {
const double h = size / 2.0;
const double pts[3][2] = {{h, 0}, {-h, -h}, {-h, h}}; // tip and base, pointing right
double v[3][2];
for (int i = 0; i < 3; ++i) {
v[i][0] = cx + pts[i][0] * dx - pts[i][1] * dy;
v[i][1] = cy + pts[i][0] * dy + pts[i][1] * dx;
}
auto inside = [&](double x, double y) {
bool pos = false, neg = false;
for (int i = 0; i < 3; ++i) {
const double *a = v[i], *b = v[(i + 1) % 3];
const double c = (b[0] - a[0]) * (y - a[1]) - (b[1] - a[1]) * (x - a[0]);
pos |= c > 0, neg |= c < 0;
}
return !(pos && neg);
};
for (int y = int(cy - h) - 1; y <= int(cy + h) + 1; ++y)
for (int x = int(cx - h) - 1; x <= int(cx + h) + 1; ++x) {
if (x < 0 || y < 0 || x >= kW || y >= kH) continue;
int n = 0; // 4x4 samples, for smooth edges
for (int sy = 0; sy < 4; ++sy)
for (int sx = 0; sx < 4; ++sx) n += inside(x + (sx + 0.5) / 4, y + (sy + 0.5) / 4);
if (!n) continue;
const double a = n / 16.0 * alpha / 255.0;
uint8_t *p = &g_px[(size_t(y) * kW + x) * 4];
for (int c = 0; c < 3; ++c) p[c] = uint8_t(std::lround(p[c] + (255 - p[c]) * a));
}
}
void Layout() {
g_keys.clear();
const double unit = double(kW - 2 * kMargin) / kColumns, row = double(kH - kGrip - kMargin) / kRows;
for (int r = 0; r < kRows; ++r) {
double x = kMargin;
for (const KeyDef &d : kLayout[r]) {
Key k{d, int(std::lround(x + kGap / 2.0)), int(std::lround(kGrip + r * row + kGap / 2.0)),
int(std::lround(x + d.units * unit - kGap / 2.0)),
int(std::lround(kGrip + (r + 1) * row - kGap / 2.0))};
g_keys.push_back(k);
x += d.units * unit;
}
}
}
// A rounded rectangle over what's there (straight alpha, like the other panels).
void FillRounded(int x0, int y0, int x1, int y1, double radius, const uint8_t rgba[4]) {
for (int y = y0; y < y1; ++y)
for (int x = x0; x < x1; ++x) {
const double cx = std::clamp(x + 0.5, x0 + radius, x1 - radius);
const double cy = std::clamp(y + 0.5, y0 + radius, y1 - radius);
const double cover = std::clamp(radius - std::hypot(x + 0.5 - cx, y + 0.5 - cy) + 0.5, 0.0, 1.0);
if (cover <= 0) continue;
uint8_t *p = &g_px[(size_t(y) * kW + x) * 4];
const double a = rgba[3] / 255.0 * cover, below = p[3] / 255.0;
const double out = a + below * (1 - a);
for (int c = 0; c < 3; ++c)
p[c] = uint8_t(std::lround(out > 0 ? (rgba[c] * a + p[c] * below * (1 - a)) / out : 0));
p[3] = uint8_t(std::lround(out * 255));
}
}
// ---------------------------------------------------------------- buffers
struct Buffer {
gbm_bo *bo = nullptr;
int fd = -1;
vr::SharedTextureHandle_t handle = 0;
};
int g_drm = -1;
gbm_device *g_gbm = nullptr;
Buffer g_buffers[3];
int g_next = 0;
bool g_useBuffers = false;
void DropBuffers() {
for (Buffer &b : g_buffers) {
if (b.handle) vr::VRIPCResourceManager()->UnrefResource(b.handle);
if (b.fd >= 0) close(b.fd);
if (b.bo) gbm_bo_destroy(b.bo);
b = Buffer{};
}
if (g_gbm) gbm_device_destroy(g_gbm);
if (g_drm >= 0) close(g_drm);
g_gbm = nullptr, g_drm = -1, g_useBuffers = false;
}
bool MakeBuffers() {
g_drm = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
if (g_drm >= 0) g_gbm = gbm_create_device(g_drm);
for (Buffer &b : g_buffers) {
// ABGR8888 is R, G, B, A in memory, like g_px.
if (g_gbm) b.bo = gbm_bo_create(g_gbm, kW, kH, GBM_FORMAT_ABGR8888, GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
if (!b.bo || (b.fd = gbm_bo_get_fd(b.bo)) < 0) break;
vr::DmabufAttributes_t a{};
a.unWidth = kW, a.unHeight = kH;
a.unDepth = a.unMipLevels = a.unArrayLayers = a.unSampleCount = 1;
a.unFormat = DRM_FORMAT_ABGR8888;
a.ulModifier = DRM_FORMAT_MOD_LINEAR;
a.unPlaneCount = 1;
a.plane[0].unOffset = gbm_bo_get_offset(b.bo, 0);
a.plane[0].unStride = gbm_bo_get_stride(b.bo);
a.plane[0].nFd = b.fd;
if (!vr::VRIPCResourceManager()->ImportDmabuf(vr::VRApplication_Overlay, &a, &b.handle)) b.handle = 0;
if (!b.handle) break;
}
g_useBuffers = g_buffers[2].handle != 0;
if (!g_useBuffers) {
std::printf("keyboard: no shared buffers; falling back to SetOverlayRaw (the panel flickers)\n");
DropBuffers();
}
return g_useBuffers;
}
// g_px to the panel: into the next buffer (premultiplied, as the flag says), then shown.
void Present() {
if (!g_useBuffers) {
vr::VROverlay()->SetOverlayRaw(g_overlay, g_px.data(), kW, kH, 4);
return;
}
Buffer &b = g_buffers[g_next];
g_next = (g_next + 1) % 3;
uint32_t stride = 0;
void *mapping = nullptr;
auto *dst = static_cast<uint8_t *>(gbm_bo_map(b.bo, 0, 0, kW, kH, GBM_BO_TRANSFER_WRITE, &stride, &mapping));
if (!dst) return;
for (int y = 0; y < kH; ++y) {
const uint8_t *src = &g_px[size_t(y) * kW * 4];
uint8_t *row = dst + size_t(y) * stride;
for (int x = 0; x < kW * 4; x += 4) {
const unsigned a = src[x + 3];
row[x] = uint8_t(src[x] * a / 255), row[x + 1] = uint8_t(src[x + 1] * a / 255);
row[x + 2] = uint8_t(src[x + 2] * a / 255), row[x + 3] = uint8_t(a);
}
}
gbm_bo_unmap(b.bo, mapping);
vr::Texture_t tex = {&b.handle, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma};
vr::VROverlay()->SetOverlayTexture(g_overlay, &tex);
}
void Draw() {
g_px.assign(size_t(kW) * kH * 4, 0);
const uint8_t back[4] = {20, 22, 26, 225};
FillRounded(0, 0, kW, kH, 22, back);
const bool grip = g_hoverGrip || g_dragDevice != vr::k_unTrackedDeviceIndexInvalid;
const uint8_t bar[4] = {220, 222, 228, uint8_t(grip ? 235 : 120)};
FillRounded(kW / 2 - 130, kGrip / 2 - 5, kW / 2 + 130, kGrip / 2 + 7, 6, bar);
const bool shift = g_latched[Shift];
const int row = (kH - kGrip - kMargin) / kRows;
for (size_t i = 0; i < g_keys.size(); ++i) {
const Key &k = g_keys[i];
const int mod = ModOf(k.def.code);
const bool latched = mod >= 0 && g_latched[mod];
uint8_t face[4] = {58, 62, 70, 255};
if (latched) face[0] = 26, face[1] = 115, face[2] = 232; // the blue the other controls use when armed
if (int(i) == g_down) face[0] = 26, face[1] = 115, face[2] = 232;
else if (int(i) == g_hover) for (int c = 0; c < 3; ++c) face[c] = uint8_t(std::min(255, face[c] + 38));
FillRounded(k.x0, k.y0, k.x1, k.y1, 10, face);
const int cx = (k.x0 + k.x1) / 2, cy = (k.y0 + k.y1) / 2;
switch (k.def.code) {
case KEY_UP: DrawArrow(cx, cy, row / 3, 0, -1, 235); continue;
case KEY_DOWN: DrawArrow(cx, cy, row / 3, 0, 1, 235); continue;
case KEY_LEFT: DrawArrow(cx, cy, row / 3, -1, 0, 235); continue;
case KEY_RIGHT: DrawArrow(cx, cy, row / 3, 1, 0, 235); continue;
}
const char *label = shift && k.def.shifted ? k.def.shifted : k.def.label;
const bool word = Codepoints(label).size() > 1;
DrawText(label, int(row * (word ? 0.3 : 0.46)), cx, cy, 235);
}
Present();
g_dirty = false;
}
int KeyAt(double x, double y) {
for (size_t i = 0; i < g_keys.size(); ++i) {
const Key &k = g_keys[i];
if (x >= k.x0 - kGap / 2.0 && x < k.x1 + kGap / 2.0 && y >= k.y0 - kGap / 2.0 && y < k.y1 + kGap / 2.0)
return int(i);
}
return -1;
}
// ---------------------------------------------------------------- carrying it
vr::HmdMatrix34_t Mul(const vr::HmdMatrix34_t &a, const vr::HmdMatrix34_t &b) {
vr::HmdMatrix34_t r{};
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 4; ++j) {
double v = j == 3 ? a.m[i][3] : 0;
for (int k = 0; k < 3; ++k) v += a.m[i][k] * b.m[k][j];
r.m[i][j] = float(v);
}
return r;
}
vr::HmdMatrix34_t Inverse(const vr::HmdMatrix34_t &a) { // rigid: R^T, -R^T t
vr::HmdMatrix34_t r{};
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j) r.m[i][j] = a.m[j][i];
for (int i = 0; i < 3; ++i) r.m[i][3] = -(r.m[i][0] * a.m[0][3] + r.m[i][1] * a.m[1][3] + r.m[i][2] * a.m[2][3]);
return r;
}
bool DevicePose(vr::TrackedDeviceIndex_t dev, vr::HmdMatrix34_t *out) {
vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount];
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, poses, vr::k_unMaxTrackedDeviceCount);
if (dev >= vr::k_unMaxTrackedDeviceCount || !poses[dev].bPoseIsValid) return false;
*out = poses[dev].mDeviceToAbsoluteTracking;
return true;
}
void Place(const vr::HmdMatrix34_t &pose) {
g_pose = pose;
vr::VROverlay()->SetOverlayTransformAbsolute(g_overlay, vr::TrackingUniverseStanding, &g_pose);
}
void StopCarrying() {
if (g_dragDevice == vr::k_unTrackedDeviceIndexInvalid) return;
g_dragDevice = vr::k_unTrackedDeviceIndexInvalid;
g_dirty = true;
}
bool Create() {
if (g_overlay != vr::k_ulOverlayHandleInvalid) return true;
if (vr::VROverlay()->CreateOverlay("frametop.keyboard", "Frametop keyboard", &g_overlay) != vr::VROverlayError_None)
return false;
LoadFont();
Layout();
vr::VROverlay()->SetOverlayWidthInMeters(g_overlay, float(kWidthMetres));
vr::VROverlay()->SetOverlayInputMethod(g_overlay, vr::VROverlayInputMethod_Mouse);
vr::HmdVector2_t scale{{float(kW), float(kH)}};
vr::VROverlay()->SetOverlayMouseScale(g_overlay, &scale);
vr::VROverlay()->SetOverlaySortOrder(g_overlay, 15); // over the screens and their controls
if (MakeBuffers()) vr::VROverlay()->SetOverlayFlag(g_overlay, vr::VROverlayFlags_IsPremultiplied, true);
return true;
}
} // namespace
bool Show(const vr::HmdMatrix34_t &pose) {
if (!Create()) return false;
Place(pose);
if (!g_shown) {
std::fill(std::begin(g_latched), std::end(g_latched), false);
g_hover = g_down = -1;
g_hoverGrip = false;
g_dragDevice = vr::k_unTrackedDeviceIndexInvalid;
Draw();
vr::VROverlay()->ShowOverlay(g_overlay);
g_shown = true;
}
return true;
}
const vr::HmdMatrix34_t &Pose() { return g_pose; }
void EndDragBy(uint32_t device) {
if (device == g_dragDevice) StopCarrying();
}
void Hide() {
StopCarrying();
if (!g_shown) return;
vr::VROverlay()->HideOverlay(g_overlay);
g_shown = false;
}
bool Shown() { return g_shown; }
void SetLasers(bool on) {
if (Create()) vr::VROverlay()->SetOverlayFlag(g_overlay, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, on);
}
void Poll(void (*handle)(const Event &, void *), void *data) {
if (g_overlay == vr::k_ulOverlayHandleInvalid) return;
auto send = [&](uint32_t code, bool pressed) { handle(Event{Event::Key, code, pressed}, data); };
// Let go of the key held down: it, then the latched modifiers it was typed with.
auto release = [&]() {
if (g_down < 0) return;
const uint32_t code = g_keys[g_down].def.code;
g_down = -1;
g_dirty = true;
if (code == kHide || ModOf(code) >= 0) return;
send(code, false);
for (int m = kMods - 1; m >= 0; --m)
if (g_latched[m]) send(kModCode[m], false), g_latched[m] = false;
};
vr::VREvent_t ev;
while (vr::VROverlay()->PollNextOverlayEvent(g_overlay, &ev, sizeof ev)) {
const double x = ev.data.mouse.x, y = kH - ev.data.mouse.y; // OpenVR's mouse origin is bottom left
switch (ev.eventType) {
case vr::VREvent_MouseMove: {
const int k = KeyAt(x, y);
if (k != g_hover) g_hover = k, g_dirty = true;
if ((y < kGrip) != g_hoverGrip) g_hoverGrip = y < kGrip, g_dirty = true;
break;
}
case vr::VREvent_MouseButtonDown: {
if (ev.data.mouse.button != vr::VRMouseButton_Left) break;
release();
vr::HmdMatrix34_t dev;
if (y < kGrip && DevicePose(ev.trackedDeviceIndex, &dev)) {
g_dragDevice = ev.trackedDeviceIndex;
g_dragRel = Mul(Inverse(dev), g_pose);
g_dirty = true;
break;
}
const int k = KeyAt(x, y);
if (k < 0) break;
g_down = k;
g_dirty = true;
const uint32_t code = g_keys[k].def.code;
const int mod = ModOf(code);
if (code == kHide) {
Hide();
handle(Event{Event::Closed, 0, false}, data);
} else if (mod >= 0) {
g_latched[mod] = !g_latched[mod];
} else {
for (int m = 0; m < kMods; ++m)
if (g_latched[m]) send(kModCode[m], true);
send(code, true);
}
break;
}
case vr::VREvent_MouseButtonUp:
if (ev.data.mouse.button == vr::VRMouseButton_Left) release(), EndDragBy(ev.trackedDeviceIndex);
break;
case vr::VREvent_FocusLeave:
release();
if (g_hover >= 0) g_hover = -1, g_dirty = true;
if (g_hoverGrip) g_hoverGrip = false, g_dirty = true;
break;
default:
break;
}
}
vr::HmdMatrix34_t dev;
if (g_dragDevice != vr::k_unTrackedDeviceIndexInvalid) {
if (DevicePose(g_dragDevice, &dev)) Place(Mul(dev, g_dragRel));
else StopCarrying(); // the device went away
}
if (g_dirty && g_shown) Draw();
}
void Destroy() {
if (g_overlay != vr::k_ulOverlayHandleInvalid) vr::VROverlay()->DestroyOverlay(g_overlay);
DropBuffers();
g_overlay = vr::k_ulOverlayHandleInvalid;
g_shown = false;
}
} // namespace keyboard
+33
View File
@@ -0,0 +1,33 @@
// Frametop's keyboard (keyboard.cpp): a panel of keys that ft-screens shows for a text
// field on the desktop. Any laser or the 3D mouse types on it, as on the screens.
#pragma once
#include <openvr.h>
#include <cstdint>
namespace keyboard {
// What happened on the panel since the last Poll.
struct Event {
enum Type { Key, Closed } type;
uint32_t code; // Key: linux KEY_*
bool pressed; // Key: down or up
};
// Show the panel at `pose` (standing universe; its front faces +z), creating it the first
// time. False if SteamVR won't make the overlay.
bool Show(const vr::HmdMatrix34_t &pose);
void Hide();
bool Shown();
// Where it is now (it can be carried by its grab bar).
const vr::HmdMatrix34_t &Pose();
// A button came up on `device` somewhere else: stop carrying the panel if it was.
void EndDragBy(uint32_t device);
// Controllers' lasers work the panel with the dashboard closed, like the screens'.
void SetLasers(bool on);
// The panel's input: key presses and releases, and Closed for its Close key.
void Poll(void (*handle)(const Event &, void *), void *data);
void Destroy();
} // namespace keyboard
+93
View File
@@ -49,6 +49,7 @@
#include "vr.h"
#include "handcut.h"
#include "keyboard.h"
#include <drm_fourcc.h>
#include <openvr.h>
@@ -931,6 +932,7 @@ void FinishDrag(Screen &s, int index) {
// A button release on any of our panels ends that device's drags (it may be over another
// screen by then).
void EndDragsBy(vr::TrackedDeviceIndex_t dev) {
keyboard::EndDragBy(dev);
for (auto &[index, s] : g_screens)
if (s.drag != Drag::None && s.dragDevice == dev) FinishDrag(s, index);
}
@@ -1142,6 +1144,37 @@ void UpdateCutouts() {
}
}
// Steam in front: the dashboard (the Steam menu) is open, or Steam's own keyboard is up
// (valve.steam.gamepadui.keyboard, for text fields in Steam and the dashboard). Our
// keyboard steps aside then, and comes back where it was when Steam is out of the way; one
// asked for meanwhile appears then. Checked every 9 ticks; Steam makes its keyboard's
// overlay again now and then, so it's looked up each time.
bool g_steamInFront = false;
bool g_keyboardAside = false; // ours is waiting for Steam to get out of the way
Mat g_asidePose = Identity(); // ...and goes here then
bool SteamInFront() {
vr::VROverlayHandle_t h = vr::k_ulOverlayHandleInvalid;
// In the dashboard mode the screens only show with the dashboard, so it doesn't count.
return (g_mode != Mode::Dashboard && vr::VROverlay()->IsDashboardVisible()) ||
(vr::VROverlay()->FindOverlay("valve.steam.gamepadui.keyboard", &h) == vr::VROverlayError_None &&
vr::VROverlay()->IsOverlayVisible(h));
}
void UpdateSteamInFront() {
const bool front = SteamInFront();
if (front == g_steamInFront) return;
g_steamInFront = front;
if (front && keyboard::Shown()) {
g_asidePose = keyboard::Pose();
keyboard::Hide();
g_keyboardAside = true;
} else if (!front && g_keyboardAside) {
g_keyboardAside = false;
keyboard::Show(g_asidePose);
}
}
} // namespace
extern "C" {
@@ -1173,6 +1206,7 @@ void ft_vr_shutdown(void) {
for (auto &[dev, g] : g_guides)
for (auto o : {g.ring.overlay, g.dot.overlay}) vr::VROverlay()->DestroyOverlay(o);
for (auto &[k, h] : g_imports) vr::VRIPCResourceManager()->UnrefResource(h);
keyboard::Destroy();
g_guides.clear();
g_screens.clear();
g_imports.clear();
@@ -1386,6 +1420,30 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
// A carrying controller that goes away drops its screen.
if (ev.eventType == vr::VREvent_TrackedDeviceDeactivated) EndDragsBy(ev.trackedDeviceIndex);
}
// Our keyboard: its keys, and its Close key. It goes when the screens do.
struct Forward {
void (*handle)(const struct ft_event *, void *);
void *data;
} forward{handle, data};
keyboard::Poll(
[](const keyboard::Event &k, void *f) {
ft_event e{};
e.screen = -1;
e.type = k.type == keyboard::Event::Key ? FT_KEY : FT_KEYBOARD_CLOSED;
e.key = k.code;
e.pressed = k.pressed;
static_cast<Forward *>(f)->handle(&e, static_cast<Forward *>(f)->data);
},
&forward);
if (g_tick % 9 == 0) UpdateSteamInFront();
if ((keyboard::Shown() || g_keyboardAside) && !ModeVisible()) {
g_keyboardAside = false;
keyboard::Hide();
ft_event e{};
e.type = FT_KEYBOARD_CLOSED;
e.screen = -1;
handle(&e, data);
}
++g_tick;
UpdateGame();
UpdateArrange();
@@ -1396,6 +1454,41 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
UpdateCutouts();
}
// Our keyboard (keyboard.cpp) for a screen. It's placed where you'll reach it, not on the
// screen: kKeyboardAhead in front of you (the way your head faces, level) and
// kKeyboardBelow under your eyes, turned to face your eyes, and it stays where it opened
// (or where its grab bar carries it). With Steam in front (UpdateSteamInFront), it waits.
// Without a head pose (the headset is in standby, say) it doesn't open: anywhere else could
// be out of sight or reach. The next text field opens it.
constexpr double kKeyboardAhead = 0.7, kKeyboardBelow = 0.35;
bool ft_vr_keyboard_show(int index) {
if (g_screens.find(index) == g_screens.end()) return false;
RefreshPoses();
Mat head;
if (!DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head)) {
std::printf("keyboard: no head pose, not opened\n");
return false;
}
const double fx = -head.m[0][2], fz = -head.m[2][2], n = std::sqrt(fx * fx + fz * fz) + 1e-9;
const double at[3] = {head.m[0][3] + fx / n * kKeyboardAhead, head.m[1][3] - kKeyboardBelow,
head.m[2][3] + fz / n * kKeyboardAhead};
keyboard::SetLasers(g_lasers == Lasers::Always || (g_lasers == Lasers::OutsideGames && !g_gameRunning));
g_steamInFront = SteamInFront();
if (g_steamInFront) {
g_asidePose = FacingPose(at, head);
g_keyboardAside = true;
std::printf("keyboard: waiting for Steam to close\n");
return true;
}
return keyboard::Show(FacingPose(at, head));
}
void ft_vr_keyboard_hide(void) {
g_keyboardAside = false;
keyboard::Hide();
}
// Control commands (datagrams on @ft_screens, replies to the sender):
// place <screen> <x> <y> <z> <yaw> <pitch> <roll> centre (standing universe) and facing
// width <screen> <metres>
+8 -1
View File
@@ -17,7 +17,7 @@ struct ft_dmabuf {
int fd[4];
};
enum ft_event_type { FT_MOTION, FT_BUTTON, FT_SCROLL, FT_LEAVE, FT_QUIT };
enum ft_event_type { FT_MOTION, FT_BUTTON, FT_SCROLL, FT_LEAVE, FT_QUIT, FT_KEY, FT_KEYBOARD_CLOSED };
struct ft_event {
enum ft_event_type type;
@@ -26,6 +26,7 @@ struct ft_event {
uint32_t button; // FT_BUTTON: linux BTN_*
bool pressed;
double dx, dy; // FT_SCROLL: notches (positive dy: scroll down)
uint32_t key; // FT_KEY: linux KEY_* from our keyboard (pressed: down or up)
};
bool ft_vr_init(void);
@@ -44,6 +45,12 @@ bool ft_vr_screen_present(int index, const void *key, const struct ft_dmabuf *bu
void ft_vr_forget(const void *key);
// Poll panel input and SteamVR events.
void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data);
// Our keyboard (keyboard.cpp), for typing on screen `index`: its keys arrive as FT_KEY
// events, and FT_KEYBOARD_CLOSED when its Close key is pressed or the screens hide. False if
// it can't be shown or there's no head pose. With the Steam menu or Steam's own keyboard
// up, it waits and appears when they're gone.
bool ft_vr_keyboard_show(int index);
void ft_vr_keyboard_hide(void);
// A command from the control socket (@ft_screens); writes the reply (see vr.cpp).
void ft_vr_command(const char *command, char *reply, int reply_size);