lantern: deliver Tab/Shift+Tab to the web content instead of GTK focus-nav
GTK4 reserves Tab (focus-next) and Shift+Tab / ISOLeftTab (focus-prev) for widget focus navigation and consumes them BEFORE the embedded WebKitWebView's web content ever receives a keydown. A keyboard-first web app (Slate) therefore never sees Tab, and GTK moves focus right out of the view — e.g. Shift+Tab in a Slate terminal made the whole pane lose focus until you clicked back in. No page-side JS can fix this: the event does not reach the page at all (verified live — the page's keydown listener fires in Chrome but never in Lantern).
Fix: install a CAPTURE-phase GtkEventControllerKey on the window. on-key-pressed intercepts GDKKEYTab / GDKKEYISOLeftTab, forwards a synthetic keydown into the page (dispatched on the focused element / the app .frame, so the app's own keymap routes it — terminal backtab e[Z, editor indent, ...), and returns TRUE to consume the GTK event so focus stays in the view. The keyval alone distinguishes plain Tab from Shift+Tab (ISOLeftTab), so no GdkModifierType decode is needed. Every other key returns FALSE and flows to WebKit unchanged; on any error the guard returns FALSE, falling back to GTK's default rather than swallowing a keystroke.
v1 single-window: the callback reads main-view, set in create-window (matches the existing single-window callback shape). Confirmed working end-to-end in a Slate terminal (Claude) in a native Lantern window.
lantern/src/lantern/backend/gtk.sgl | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)lantern/src/lantern/backend/gtk.sglmodified
;; GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - app CSS beats the theme's window bg. (define STYLE-PRIORITY-APPLICATION 600) ;; GtkPropagationPhase: CAPTURE = 1 (top-down, before the target + before GTK's ;; own Tab focus-navigation default fires). (define GTK-PHASE-CAPTURE 1) ;; GDK keyvals for the focus-navigation keys GTK4 reserves. Shift+Tab arrives ;; as ISO_Left_Tab (a distinct keyval), so the keyval alone tells plain Tab ;; from Shift+Tab — no GdkModifierType decode needed. (define GDK-KEY-Tab #xff09) (define GDK-KEY-ISO-Left-Tab #xfe20) ;; ---- lazily-bound FFI (all #f until ensure!) ------------------------- (define *ready* #f) (define gtk-css-provider-load-from-string #f) (define gtk-style-context-add-provider-for-display #f) (define gdk-display-get-default #f) ;; Key event controller (to intercept Tab/Shift+Tab before GTK's focus-nav). (define gtk-event-controller-key-new #f) (define gtk-event-controller-set-propagation-phase #f) (define gtk-widget-add-controller #f) (define webkit-web-context-get-default #f) (define webkit-web-context-get-security-manager #f) (define message-cb #f) (define policy-cb #f) (define close-cb #f) (define keypress-cb #f) ;; ---- per-process backend state --------------------------------------- ;; ;; The display-wide CSS provider that clears the window background for ;; transparency. Installed once, rooted here for the process lifetime. (define *transparency-provider* #f) ;; The single window's web view, so the capture-phase key handler can forward ;; Tab/Shift+Tab into the page (v1 is single-window; see the callback note). (define *main-view* #f) (define (path-of request) (let ((p (webkit-uri-scheme-request-get-path request))) (set! *quit-requested* #t) #f))) ;; key-pressed on a CAPTURE-phase controller on the window (5-arg: ;; controller keyval keycode state user-data -> gboolean). ;; ;; GTK4 reserves Tab (focus-next) and Shift+Tab / ISO_Left_Tab (focus-prev) ;; for widget focus navigation and consumes them BEFORE the WebKitWebView's ;; web content ever sees a keydown. A keyboard-first web app (Slate) therefore ;; never receives Tab, and GTK moves focus right out of the view (the terminal ;; "loses focus" on Shift+Tab until you click back in). No page-side JS can fix ;; this — the event doesn't reach the page. So intercept the two focus-nav keys ;; HERE, forward a synthetic keydown into the page (Slate's own keymap then ;; routes it — terminal backtab \e[Z, editor indent, etc.), and return #t to ;; consume the GTK event so focus stays in the view. Every other key returns #f ;; and flows to WebKit normally. The keyval distinguishes plain Tab from ;; Shift+Tab (ISO_Left_Tab), so no modifier decode is needed. On any error the ;; guard returns #f — i.e. fall back to GTK's default (focus moves), never a ;; swallowed keystroke. (define (on-key-pressed controller keyval keycode state user-data) (guarded "key-pressed" #f (lambda () (if (and *main-view* (or (= keyval GDK-KEY-Tab) (= keyval GDK-KEY-ISO-Left-Tab))) (let ((shift (if (= keyval GDK-KEY-ISO-Left-Tab) "true" "false"))) (webkit-web-view-eval-js *main-view* (string-append "(function(){var f=document.querySelector('.frame');if(!f)return;" "var a=document.activeElement;" "var t=(a&&f.contains(a))?a:f;" "t.dispatchEvent(new KeyboardEvent('keydown',{key:'Tab',code:'Tab'," "keyCode:9,which:9,shiftKey:" shift ",bubbles:true,cancelable:true}));})();") -1 NULL NULL NULL NULL NULL) #t) #f)))) ;; GdkRGBA = 4 floats in GTK4. set_background_color copies, so free at once. (define (set-view-background view r g b a) (let ((rgba (dict->c-struct (dict red: r green: g blue: b alpha: a) gdk-rgba-layout))) (set! gtk-style-context-add-provider-for-display (c-function libgtk "gtk_style_context_add_provider_for_display" (list ffi/pointer ffi/pointer ffi/uint) ffi/void)) (set! gdk-display-get-default (c-function libgtk "gdk_display_get_default" (list) ffi/pointer)) (set! gtk-event-controller-key-new (c-function libgtk "gtk_event_controller_key_new" (list) ffi/pointer)) (set! gtk-event-controller-set-propagation-phase (c-function libgtk "gtk_event_controller_set_propagation_phase" (list ffi/pointer ffi/int) ffi/void)) (set! gtk-widget-add-controller (c-function libgtk "gtk_widget_add_controller" (list ffi/pointer ffi/pointer) ffi/void)) (set! webkit-web-context-get-default (c-function libwebkit "webkit_web_context_get_default" (list) ffi/pointer)) (set! webkit-web-context-get-security-manager (set! message-cb (c-callback (list ffi/pointer ffi/pointer ffi/pointer) ffi/void on-script-message)) (set! policy-cb (c-callback (list ffi/pointer ffi/pointer ffi/int ffi/pointer) ffi/bool on-decide-policy)) (set! close-cb (c-callback (list ffi/pointer ffi/pointer) ffi/bool on-close-request)) ;; GtkEventControllerKey::key-pressed = (controller keyval keycode state user-data) -> gboolean (set! keypress-cb (c-callback (list ffi/pointer ffi/uint ffi/uint ffi/uint ffi/pointer) ffi/bool on-key-pressed)) (set! *ready* #t)))) ;; Origin gating: every navigation attempt passes through decide-policy. (signal-connect! view "decide-policy" policy-cb) (signal-connect! win "close-request" close-cb) ;; Deliver Tab/Shift+Tab to the web content instead of letting GTK4 ;; consume them for widget focus navigation (which yanks focus out of ;; the view). A capture-phase key controller on the window fires before ;; GTK's focus-nav; on-key-pressed forwards those keys into the page and ;; consumes the GTK event. (v1 single-window: the callback reads *main-view*.) (set! *main-view* view) (let ((kc (gobject-borrow (gtk-event-controller-key-new)))) (gtk-event-controller-set-propagation-phase kc GTK-PHASE-CAPTURE) (signal-connect! kc "key-pressed" keypress-cb) (gtk-widget-add-controller win kc)) (dict win: win view: view ucm: ucm))) (define (load-uri window uri)