feat(input): opt-in WebKitGTK focus repaint workaround for Linux (#342, #782) (#884)

* feat(input): opt-in WebKitGTK focus repaint workaround for Linux (#342, #782)

Some Linux users on WebKitGTK 2.50.x report a freeze when clicking
text fields: the field receives focus (right-click paste still works)
but the canvas never redraws until the window is resized. Likely a
layer-flush / composition scheduling bug in 2.50.x's Skia rendering
pipeline.

Off by default. When enabled in Settings -> System -> Behavior, every
input/textarea focus triggers a sync reflow read plus a one-frame
translateZ(0) toggle on the input's parent so WebKit re-evaluates the
layer tree. Side-effect: search-icon siblings flicker briefly on
focus -- accepted trade-off, only paid by users who opt in.

The toggle row is gated on IS_LINUX and sits next to the existing
Linux WebKitGTK options. The effect subscribes to the auth store and
re-attaches the focusin handler whenever the flag flips, so toggling
off cleanly removes the listener.

* i18n(settings): linux input repaint toggle in all locales

Adds linuxWebkitInputForceRepaint label + description across en, de,
fr, es, nl, nb, ro, zh, ru.

* docs(release): CHANGELOG and credits for linux input freeze workaround (PR #884)
This commit is contained in:
Frank Stellmacher
2026-05-28 20:01:25 +02:00
committed by GitHub
parent 9fa086c428
commit 403979b35d
16 changed files with 77 additions and 0 deletions
+26
View File
@@ -164,6 +164,32 @@ export function AppShell() {
return () => window.removeEventListener('psy:toggle-sidebar', onToggleSidebar);
}, [isSidebarCollapsed, setSidebarCollapsed]);
// Workaround for WebKitGTK 2.50.x text-input repaint hang on
// Linux Mint / Ubuntu 24.04 (issues #342, #782). When opted in,
// nudge WebKit awake on every input/textarea focus via a sync
// reflow read plus a one-frame translateZ(0) toggle on the input's
// parent so the rendering pipeline re-evaluates the layer tree.
// Side-effect: search magnifier flickers briefly on focus.
const linuxWebkitInputForceRepaint = useAuthStore(s => s.linuxWebkitInputForceRepaint);
useEffect(() => {
if (!linuxWebkitInputForceRepaint) return;
const handler = (e: FocusEvent) => {
const target = e.target;
if (!(target instanceof HTMLElement)) return;
const tag = target.tagName;
if (tag !== 'INPUT' && tag !== 'TEXTAREA') return;
const layerHost = (target.parentElement as HTMLElement | null) ?? target;
void layerHost.offsetHeight;
const previous = layerHost.style.transform;
layerHost.style.transform = 'translateZ(0)';
requestAnimationFrame(() => {
layerHost.style.transform = previous;
});
};
document.addEventListener('focusin', handler, true);
return () => document.removeEventListener('focusin', handler, true);
}, [linuxWebkitInputForceRepaint]);
const {
queueWidth,
isDraggingQueue,