Skip to content

[bug][cef][linux] Keyboard input fails at startup or after switching back to the app #16251

Description

@mahendrimd

Describe the bug

On Fedora 44/KDE Wayland with XWayland, a manually created, initially hidden Tauri/CEF window can stop receiving input from a physical keyboard at startup or after switching back from another application. Clicking an input changes the focused element in the DOM, but typing still does not work. When this happens, native X11 focus remains on GTK's focus proxy. Explicitly focusing the CEF browser window restored typing in the tested configurations.

Test environment:

  • Fedora 44, KDE Plasma, Wayland session with XWayland (DISPLAY=:0).
  • Tauri revision: a8703ee487c659efbebb27c799752d523a6d09a1.
  • tauri-runtime-cef 3.0.0-alpha.5; CEF 152.3.0.
  • Debug builds; physical keyboard input.

Reproduction

Create the project using the source files below. It contains one webview, two HTML text inputs, and event counters. It uses no application plugins or frontend framework.

Install the CLI from the same Tauri revision:

cargo install tauri-cli \
  --git https://github.com/tauri-apps/tauri \
  --rev a8703ee487c659efbebb27c799752d523a6d09a1 \
  --locked

Run this command from the project directory, leaving all other CEF_REPRO_* variables unset:

CEF_REPRO_MANUAL_WINDOW=1 \
  cargo tauri dev --no-watch --port 1431 --config variants/hidden-startup.json

The setup callback manually creates an initially hidden window without window decorations (create: false, visible: false), then calls show() and set_focus().

  1. Click an input and type using the physical keyboard.
  2. Switch to another application, then return to the reproduction app.
  3. Click either input and type again. Repeat steps 2 and 3 several times.

Observed: Typing can fail at startup or after switching back to the app. Clicking an input still changes the focused element in the DOM, and document.hasFocus() can remain true, but the page receives no keydown, beforeinput, or input events while typing fails.

To compare with the default configuration, which creates the window automatically, unset all CEF_REPRO_* variables and run:

cargo tauri dev --no-watch --port 1431

CEF remote debugging is available on port 4001.

Expected behavior

The focused input should accept typing at startup and after switching back to the app.

Full tauri info output

The reproduction uses CEF with Tauri default features disabled.

Full environment and package output
[✘] Environment
    - OS: Fedora 44.0.0 x86_64 (X64) (KDE on wayland)
    ✘ webkit2gtk-4.1: not installed
      Visit https://v2.tauri.app/start/prerequisites/ to learn more about tauri prerequisites
    ✔ rsvg2: 2.62.4
    ✔ rustc: 1.99.0 (b940084d7 2026-09-28)
    ✔ cargo: 1.99.0 (5f94df478 2026-08-27)
    ✔ rustup: 1.29.1 (d95a37b6a 2026-08-13)
    ✔ Rust toolchain: stable-x86_64-unknown-linux-gnu (overridden by environment variable RUSTUP_TOOLCHAIN)
    - node: 24.19.0
    - pnpm: 12.9.1
    - npm: 12.1.0
    - bun: 1.4.3

[-] Packages
    - tauri 🦀: git+https://github.com/tauri-apps/tauri?rev=a8703ee487c659efbebb27c799752d523a6d09a1#a8703ee487c659efbebb27c799752d523a6d09a1 (3.0.0-alpha.4)
    - tauri-build 🦀: git+https://github.com/tauri-apps/tauri?rev=a8703ee487c659efbebb27c799752d523a6d09a1#a8703ee487c659efbebb27c799752d523a6d09a1 (3.0.0-alpha.3)
    - tauri-runtime-wry 🦀: No version detected
    - tauri-runtime-cef 🦀: git+https://github.com/tauri-apps/tauri?rev=a8703ee487c659efbebb27c799752d523a6d09a1#a8703ee487c659efbebb27c799752d523a6d09a1 (3.0.0-alpha.5)
    - wry 🦀: No version detected
    - tao 🦀: No version detected
    - cef 🦀: 152.3.0+152.0.6, (outdated, latest: 154.5.0+154.0.34)
    - tauri-cli 🦀: 3.0.0-alpha.4

[-] Plugins

[-] App
    - build-type: build
    - CSP: unset
    - frontendDist: ../ui

Stack trace

Not applicable; no crash occurs.

Additional context

Related upstream report

Tauri's CEF tracking discussion describes keyboard input failing after switching back to the app and the same native focus workaround, referencing Readest issue #6397. In that report, clicking restores typing; in this reproduction, it does not. It is not yet clear whether both reports have the same underlying cause.

Isolation results

Comparison Observed result
Create the window automatically Typing worked after switching away and back.
Create the window manually, initially hidden Typing failed.
Disable Vulkan flags, omit startup maximization, or enable window decorations Typing still failed.
Omit startup window.set_focus() Typing worked at startup in one comparison, but failed after switching away and back.
Call BrowserHost.set_focus() on app window focus changes Typing could still fail.
Call BrowserHost.set_focus() and also set native keyboard focus using XSetInputFocus Typing worked in every tested switch away and back.

When typing failed, X11 focus remained on GTK's small focus child window, a sibling of the CEF host window. With automatic window creation, clicking an input moved X11 focus to the CEF browser child window. The reproduction has not been reduced further.

Experimental workaround

When the app window gains focus, the callback calls BrowserHost.set_focus() and uses XSetInputFocus to focus the native CEF browser window. It runs inside with_webview on CEF's UI thread, uses CEF's X display, and checks that the browser window is mapped and viewable before setting X11 focus.

CEF_REPRO_MANUAL_WINDOW=1 \
CEF_REPRO_MAXIMIZE_BEFORE_SHOW=1 \
CEF_REPRO_VULKAN=1 \
CEF_REPRO_FORWARD_FOCUS=1 \
CEF_REPRO_NATIVE_FOCUS=1 \
  cargo tauri dev --no-watch --port 1431 --config variants/hidden-startup.json

Typing with a physical keyboard worked at startup and after every tested switch away and back, including at least three cycles with the initially hidden, maximized window, window decorations disabled, and Vulkan/WebGPU flags enabled. Typing also worked in a simpler configuration with window decorations enabled. X11 focus returned to the CEF browser child window.

These results suggest a problem with native keyboard focus handling in the CEF runtime. The specific GTK/winit/CEF mechanism is still unknown. The workaround was tested with one webview and is not a complete upstream fix.

Standalone reproduction source

Source files and icon setup (Linux)

Create the files at the paths shown below in an empty directory. The project uses no application plugins or frontend framework.

src-tauri/Cargo.toml

[package]
name = "cef-typing-repro"
version = "0.1.0"
edition = "2024"

[dependencies]
cef = "=152.3.0"
x11-dl = "2.21"
tauri = { git = "https://github.com/tauri-apps/tauri", rev = "a8703ee487c659efbebb27c799752d523a6d09a1", default-features = false , features = [] }
tauri-runtime-cef = { git = "https://github.com/tauri-apps/tauri", rev = "a8703ee487c659efbebb27c799752d523a6d09a1" }

[build-dependencies]
tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "a8703ee487c659efbebb27c799752d523a6d09a1", features = [] }

[patch.crates-io]
tauri = { git = "https://github.com/tauri-apps/tauri", rev = "a8703ee487c659efbebb27c799752d523a6d09a1" }
tauri-utils = { git = "https://github.com/tauri-apps/tauri", rev = "a8703ee487c659efbebb27c799752d523a6d09a1" }

[profile.dev]
debug = 1

src-tauri/build.rs

fn main() {
    tauri_build::build();
}

src-tauri/tauri.conf.json

{
  "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/a8703ee487c659efbebb27c799752d523a6d09a1/crates/tauri-schema-generator/schemas/config.schema.json",
  "productName": "CEF keyboard reproduction",
  "version": "0.1.0",
  "identifier": "dev.koharu.cef-typing-repro",
  "build": {
    "frontendDist": "../ui"
  },
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "CEF keyboard reproduction",
        "width": 720,
        "height": 520
      }
    ]
  },
  "bundle": {
    "active": false
  }
}

src-tauri/src/main.rs

use cef::{ImplBrowser, ImplBrowserHost};
use tauri_runtime_cef::{Cef, CefRuntime, RemoteDebugging};
mod native_focus;

#[tauri_runtime_cef::cef_entry_point]
fn main() {
    let manual_window = std::env::var_os("CEF_REPRO_MANUAL_WINDOW").is_some();
    let forward_focus = std::env::var_os("CEF_REPRO_FORWARD_FOCUS").is_some();
    let native_focus = std::env::var_os("CEF_REPRO_NATIVE_FOCUS").is_some();
    let cef = Cef::default().remote_debugging(RemoteDebugging::Port {
        port: 4001,
        allowed_origins: Vec::new(),
    });
    #[cfg(target_os = "linux")]
    let cef = if std::env::var_os("CEF_REPRO_VULKAN").is_some() {
        cef.enable_features(["Vulkan", "VulkanFromANGLE"])
            .command_line_args([
                ("--enable-unsafe-webgpu", None),
                ("use-angle", Some("vulkan")),
                ("--ozone-platform", Some("x11")),
            ])
    } else {
        cef
    };

    tauri::Builder::<CefRuntime>::new()
        .runtime(cef)
        .on_window_event(move |window, event| {
            if forward_focus {
                if let tauri::WindowEvent::Focused(focused) = event {
                    let focused = *focused;
                    for webview in window.webviews() {
                        let _ = webview.with_webview(move |platform| {
                            if let Some(host) = platform.browser().host() {
                                host.set_focus(i32::from(focused));
                                if native_focus && focused {
                                    native_focus::focus_browser(host.window_handle() as _);
                                }
                            }
                        });
                    }
                }
            }
        })
        .setup(move |app| {
            if manual_window {
                let config = app
                    .config()
                    .app
                    .windows
                    .iter()
                    .find(|window| window.label == "main")
                    .expect("the main window config is required");
                let window = tauri::WebviewWindowBuilder::from_config(app, config)?.build()?;
                if std::env::var_os("CEF_REPRO_MAXIMIZE_BEFORE_SHOW").is_some() {
                    window.maximize()?;
                }
                window.show()?;
                if std::env::var_os("CEF_REPRO_SKIP_WINDOW_FOCUS").is_none() {
                    window.set_focus()?;
                }
            }
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("failed to run the CEF keyboard reproduction");
}

src-tauri/src/native_focus.rs

use std::mem::MaybeUninit;
use x11_dl::xlib;

// This experiment runs only inside with_webview's CEF UI-thread callback.
pub fn focus_browser(window: std::os::raw::c_ulong) {
    if window == 0 {
        return;
    }
    let Ok(x) = xlib::Xlib::open() else {
        return;
    };
    unsafe {
        // CEF owns this display; keep the borrow inside its UI-thread callback.
        let display = cef::get_xdisplay().cast::<xlib::Display>();
        if display.is_null() {
            return;
        }
        let mut attributes = MaybeUninit::<xlib::XWindowAttributes>::uninit();
        if (x.XGetWindowAttributes)(display, window, attributes.as_mut_ptr()) != 0
            && attributes.assume_init().map_state == xlib::IsViewable
        {
            (x.XSetInputFocus)(display, window, xlib::RevertToParent, xlib::CurrentTime);
            (x.XFlush)(display);
        }
    }
}

ui/index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>CEF keyboard reproduction</title>
    <style>
      body { font: 18px system-ui, sans-serif; max-width: 620px; margin: 32px auto; padding: 0 24px; }
      label { display: block; margin-top: 20px; }
      input { box-sizing: border-box; width: 100%; padding: 12px; font: inherit; margin-top: 8px; }
      pre { padding: 16px; background: #eee; white-space: pre-wrap; }
      button { padding: 8px 12px; font: inherit; }
    </style>
  </head>
  <body>
    <h1>CEF keyboard reproduction</h1>
    <p>Click a field and type <code>hello</code>. Switch to another application, return, and type again. Test Tab and Backspace too.</p>
    <label for="first">First text field</label>
    <input id="first" type="text" autocomplete="off">
    <label for="second">Second text field</label>
    <input id="second" type="text" autocomplete="off">
    <pre id="status" aria-live="polite"></pre>
    <button id="reset" type="button">Reset counters</button>
    <script>
      const counts = { keydown: 0, beforeinput: 0, input: 0, keyup: 0 };
      let lastEvent = null;
      const status = document.getElementById('status');
      function render() {
        status.textContent = JSON.stringify({
          documentFocused: document.hasFocus(),
          activeElement: document.activeElement?.id || document.activeElement?.tagName,
          counts,
          lastEvent,
        }, null, 2);
      }
      for (const type of Object.keys(counts)) {
        document.addEventListener(type, event => {
          counts[type]++;
          lastEvent = { type, key: event.key, trusted: event.isTrusted };
          render();
        }, true);
      }
      document.addEventListener('focusin', render);
      document.addEventListener('focusout', render);
      window.addEventListener('focus', render);
      window.addEventListener('blur', render);
      document.getElementById('reset').addEventListener('click', () => {
        for (const type of Object.keys(counts)) counts[type] = 0;
        lastEvent = null;
        render();
      });
      render();
    </script>
  </body>
</html>

variants/hidden-startup.json

{
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "CEF keyboard reproduction: Koharu window settings",
        "create": false,
        "width": 1024,
        "height": 768,
        "minWidth": 900,
        "minHeight": 600,
        "center": true,
        "decorations": false,
        "transparent": false,
        "visible": false
      }
    ]
  }
}

Create the required 32×32 RGBA icon:

mkdir -p src-tauri/icons
printf '%s' 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVR4nO3OIQEAAAgDMKIRhoj0gxg3E/Ornr2kEhAQEBAQEBAQEBAQEBAQSAceIftciLsLRA0AAAAASUVORK5CYII=' | base64 -d > src-tauri/icons/icon.png

The optional CEF_REPRO_* environment variables in main.rs select the configurations described above. Setting only CEF_REPRO_MANUAL_WINDOW=1 leaves CEF focus forwarding and the native X11 focus workaround disabled. The workaround command above enables them with CEF_REPRO_FORWARD_FOCUS=1 and CEF_REPRO_NATIVE_FOCUS=1.

Activity

  1. changed the title [-][bug][cef][linux] Keyboard input stops after window activation despite DOM focus[/-] [+][bug][cef][linux] Keyboard input fails at startup or after switching back to the app[/+] on Oct 11, 2026
  2. mahendrimd commented on Oct 12, 2026

    @mahendrimd
    Author

    Further testing in Koharu on the same Fedora 44 KDE Wayland/XWayland environment found that manual window creation can work without the native focus workaround. Tauri/CEF dependencies are unchanged.

    All variants retained manual creation (create: false) with no application-level CEF/X11 focus workaround:

    Window configuration and startup calls Physical typing
    visible: false, followed by show(), without set_focus() Still failed
    visible: true, followed by set_focus(), without show() Initially worked, then failed
    visible: true, without either startup call Worked across repeated launches and application switches

    Initial focus remains enabled (focus: true, Tauri’s default). The working variant is implemented in koharu-rs/koharu#1186.

    Later activation behaved differently: repeatedly invoking the existing-window handler directly through D-Bus, including its unminimize(), show(), and set_focus() calls, did not reproduce the typing failure.

    I haven’t repeated these additional comparisons in the standalone reproduction yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions