ibus wayland fix

This commit is contained in:
Skyler Lehmkuhl 2026-02-16 03:36:31 -05:00
parent 978ec6c870
commit eb1756df3f
4 changed files with 378 additions and 18 deletions

View File

@ -4332,6 +4332,15 @@ dependencies = [
"env_logger",
]
[[package]]
name = "text_input_test"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
]
[[package]]
name = "thiserror"
version = "1.0.66"

View File

@ -106,6 +106,14 @@ pub struct State {
allow_ime: bool,
ime_rect_px: Option<egui::Rect>,
/// When true, IME events are completely ignored and text input relies solely
/// on keyboard events. This is a workaround for systems where IME causes issues,
/// such as IBus on Wayland where each character triggers an IME commit that
/// disrupts normal typing flow.
///
/// See <https://github.com/emilk/egui/issues/7485>
ime_disabled: bool,
}
impl State {
@ -148,6 +156,8 @@ impl State {
allow_ime: false,
ime_rect_px: None,
ime_disabled: should_disable_ime_for_buggy_systems(),
};
slf.egui_input
@ -205,6 +215,30 @@ impl State {
self.allow_ime = allow;
}
/// Returns whether IME is disabled as a workaround for buggy systems.
///
/// When IME is disabled, text input relies solely on keyboard events.
/// This is automatically enabled on systems where IME causes issues,
/// such as IBus on Wayland.
///
/// See <https://github.com/emilk/egui/issues/7485>
pub fn ime_disabled(&self) -> bool {
self.ime_disabled
}
/// Explicitly enable or disable the IME workaround.
///
/// When set to `true`, IME events are ignored and text input relies
/// solely on keyboard events. This is useful for systems where IME
/// causes issues, such as IBus on Wayland.
///
/// By default, this is automatically detected based on the environment.
///
/// See <https://github.com/emilk/egui/issues/7485>
pub fn set_ime_disabled(&mut self, disabled: bool) {
self.ime_disabled = disabled;
}
#[inline]
pub fn egui_ctx(&self) -> &egui::Context {
&self.egui_ctx
@ -350,6 +384,31 @@ impl State {
}
WindowEvent::Ime(ime) => {
// When IME is disabled as a workaround for buggy systems (like IBus on Wayland),
// we convert IME Commit events to regular Text events instead of using IME protocol.
// This works around the bug where IBus on Wayland sends each character as an
// IME commit which disrupts normal text editing flow.
// See https://github.com/emilk/egui/issues/7485
if self.ime_disabled {
// Only handle Commit events - convert them to regular Text events
if let winit::event::Ime::Commit(text) = ime {
if !text.is_empty() && text != "\n" && text != "\r" {
self.egui_input
.events
.push(egui::Event::Text(text.clone()));
return EventResponse {
repaint: true,
consumed: self.egui_ctx.wants_keyboard_input(),
};
}
}
// Ignore all other IME events (Enabled, Disabled, Preedit)
return EventResponse {
repaint: false,
consumed: false,
};
}
// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
// So no need to check is_mac_cmd.
//
@ -907,7 +966,10 @@ impl State {
self.set_cursor_icon(window, cursor_icon);
let allow_ime = ime.is_some();
// When IME is disabled as a workaround for buggy systems, don't enable IME at all.
// This ensures the system doesn't try to intercept keyboard input.
// See https://github.com/emilk/egui/issues/7485
let allow_ime = ime.is_some() && !self.ime_disabled;
if self.allow_ime != allow_ime {
self.allow_ime = allow_ime;
profiling::scope!("set_ime_allowed");
@ -915,6 +977,10 @@ impl State {
}
if let Some(ime) = ime {
// Skip IME cursor positioning if IME is disabled
if self.ime_disabled {
self.ime_rect_px = None;
} else {
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
let ime_rect_px = pixels_per_point * ime.rect;
if self.ime_rect_px != Some(ime_rect_px)
@ -933,6 +999,7 @@ impl State {
},
);
}
}
} else {
self.ime_rect_px = None;
}
@ -1073,6 +1140,71 @@ fn open_url_in_browser(_url: &str) {
}
}
/// Detects if we're running on a system where IME is known to cause issues.
///
/// Currently detects IBus on Wayland, which has a bug where each character
/// triggers an IME commit that disrupts normal typing flow.
///
/// The detection checks:
/// 1. We're on Linux
/// 2. We're running under Wayland (XDG_SESSION_TYPE=wayland or WAYLAND_DISPLAY is set)
/// 3. IBus is the active input method (GTK_IM_MODULE=ibus or IBUS_* env vars are set)
///
/// Users can override this by setting the environment variable:
/// - `EGUI_IME_DISABLED=1` to force IME disabled (use keyboard events only)
/// - `EGUI_IME_DISABLED=0` to force IME enabled (normal behavior)
///
/// See <https://github.com/emilk/egui/issues/7485>
fn should_disable_ime_for_buggy_systems() -> bool {
// Allow explicit override via environment variable
if let Ok(val) = std::env::var("EGUI_IME_DISABLED") {
return val == "1" || val.eq_ignore_ascii_case("true");
}
// Only applies to Linux
if !cfg!(target_os = "linux") {
return false;
}
// Check if we're on Wayland
let is_wayland = std::env::var("XDG_SESSION_TYPE")
.map(|v| v == "wayland")
.unwrap_or(false)
|| std::env::var("WAYLAND_DISPLAY").is_ok();
if !is_wayland {
return false;
}
// Check if IBus is the input method
// IBus can be detected via several environment variables:
// - GTK_IM_MODULE=ibus (GTK apps)
// - QT_IM_MODULE=ibus (Qt apps)
// - XMODIFIERS=@im=ibus (X11 input method)
// - IBUS_DAEMON_PID (set when ibus-daemon is running)
let is_ibus = std::env::var("GTK_IM_MODULE")
.map(|v| v == "ibus")
.unwrap_or(false)
|| std::env::var("QT_IM_MODULE")
.map(|v| v == "ibus")
.unwrap_or(false)
|| std::env::var("XMODIFIERS")
.map(|v| v.contains("ibus"))
.unwrap_or(false)
|| std::env::var("IBUS_DAEMON_PID").is_ok();
if is_ibus {
log::warn!(
"Detected IBus on Wayland - disabling IME to work around text input bug. \
Set EGUI_IME_DISABLED=0 to override. \
See https://github.com/emilk/egui/issues/7485"
);
return true;
}
false
}
/// Winit sends special keys (backspace, delete, F1, …) as characters.
/// Ignore those.
/// We also ignore '\r', '\n', '\t'.

View File

@ -0,0 +1,14 @@
[package]
name = "text_input_test"
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
publish = false
[lints]
workspace = true
[dependencies]
eframe = { workspace = true, features = ["default"] }
egui_extras = { workspace = true, features = ["default", "syntect"] }
env_logger = { workspace = true, features = ["auto-color", "humantime"] }

View File

@ -0,0 +1,205 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#![allow(rustdoc::missing_crate_level_docs)]
use eframe::egui;
fn main() -> eframe::Result {
env_logger::init();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
..Default::default()
};
eframe::run_native(
"Text Input Test - IBus/Wayland",
options,
Box::new(|_cc| Ok(Box::<TestApp>::default())),
)
}
struct TestApp {
// Single-line text inputs
single_line_1: String,
single_line_2: String,
// Multi-line text input
multi_line: String,
// Code editor content
code: String,
// Password field
password: String,
// Input event log
event_log: Vec<String>,
}
impl Default for TestApp {
fn default() -> Self {
Self {
single_line_1: String::new(),
single_line_2: "Pre-filled text".to_owned(),
multi_line: "Type multiple lines here.\nLine 2\nLine 3".to_owned(),
code: r#"fn main() {
println!("Hello, world!");
}
"#.to_owned(),
password: String::new(),
event_log: Vec::new(),
}
}
}
impl eframe::App for TestApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Log input events
ctx.input(|i| {
for event in &i.events {
match event {
egui::Event::Text(text) => {
self.event_log.push(format!(">>> TEXT: {:?}", text));
}
egui::Event::Key { key, pressed, modifiers, .. } => {
self.event_log.push(format!("KEY: {:?} pressed={} (mods: {:?})", key, pressed, modifiers));
}
egui::Event::Ime(ime) => {
// Filter out the spammy Disabled events
match ime {
egui::ImeEvent::Disabled => {} // Skip logging these
_ => self.event_log.push(format!("IME: {:?}", ime)),
}
}
egui::Event::Paste(text) => {
self.event_log.push(format!("PASTE: {:?}", text));
}
_ => {}
}
}
});
// Keep event log manageable
if self.event_log.len() > 100 {
self.event_log.drain(0..50);
}
egui::SidePanel::right("event_log").show(ctx, |ui| {
ui.heading("Input Events");
if ui.button("Clear").clicked() {
self.event_log.clear();
}
ui.separator();
egui::ScrollArea::vertical().show(ui, |ui| {
for event in self.event_log.iter().rev() {
ui.label(event);
}
});
});
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Text Input Test - IBus/Wayland Bug");
ui.label("Test various text input widgets. If IBus under Wayland is broken, typing won't work.");
// Show environment info
ui.add_space(5.0);
ui.group(|ui| {
ui.label("Environment Detection:");
let session_type = std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "unknown".to_string());
let wayland_display = std::env::var("WAYLAND_DISPLAY").is_ok();
let gtk_im = std::env::var("GTK_IM_MODULE").unwrap_or_else(|_| "not set".to_string());
let qt_im = std::env::var("QT_IM_MODULE").unwrap_or_else(|_| "not set".to_string());
let xmodifiers = std::env::var("XMODIFIERS").unwrap_or_else(|_| "not set".to_string());
let ime_override = std::env::var("EGUI_IME_DISABLED").unwrap_or_else(|_| "not set".to_string());
ui.label(format!(" XDG_SESSION_TYPE: {session_type}"));
ui.label(format!(" WAYLAND_DISPLAY set: {wayland_display}"));
ui.label(format!(" GTK_IM_MODULE: {gtk_im}"));
ui.label(format!(" QT_IM_MODULE: {qt_im}"));
ui.label(format!(" XMODIFIERS: {xmodifiers}"));
ui.label(format!(" EGUI_IME_DISABLED: {ime_override}"));
let is_wayland = session_type == "wayland" || wayland_display;
let is_ibus = gtk_im == "ibus" || qt_im == "ibus" || xmodifiers.contains("ibus");
if is_wayland && is_ibus {
ui.colored_label(egui::Color32::GREEN,
"IBus on Wayland detected - IME workaround is active!");
}
});
ui.separator();
egui::ScrollArea::vertical().show(ui, |ui| {
ui.group(|ui| {
ui.heading("Single-line Text Edits");
ui.horizontal(|ui| {
ui.label("Empty field:");
ui.text_edit_singleline(&mut self.single_line_1);
});
ui.horizontal(|ui| {
ui.label("Pre-filled:");
ui.text_edit_singleline(&mut self.single_line_2);
});
});
ui.add_space(10.0);
ui.group(|ui| {
ui.heading("Password Field");
ui.horizontal(|ui| {
ui.label("Password:");
ui.add(egui::TextEdit::singleline(&mut self.password).password(true));
});
});
ui.add_space(10.0);
ui.group(|ui| {
ui.heading("Multi-line Text Edit");
ui.add(
egui::TextEdit::multiline(&mut self.multi_line)
.desired_width(f32::INFINITY)
.desired_rows(6)
);
});
ui.add_space(10.0);
ui.group(|ui| {
ui.heading("Code Editor (with syntax highlighting)");
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), ui.style());
let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| {
let mut layout_job = egui_extras::syntax_highlighting::highlight(
ui.ctx(),
ui.style(),
&theme,
string.as_str(),
"rs",
);
layout_job.wrap.max_width = wrap_width;
ui.fonts_mut(|f| f.layout_job(layout_job))
};
ui.add(
egui::TextEdit::multiline(&mut self.code)
.font(egui::TextStyle::Monospace)
.code_editor()
.desired_width(f32::INFINITY)
.desired_rows(10)
.layouter(&mut layouter)
);
});
ui.add_space(10.0);
ui.group(|ui| {
ui.heading("Current Values");
ui.label(format!("Single line 1: {:?}", self.single_line_1));
ui.label(format!("Single line 2: {:?}", self.single_line_2));
ui.label(format!("Password length: {}", self.password.len()));
ui.label(format!("Multi-line lines: {}", self.multi_line.lines().count()));
ui.label(format!("Code lines: {}", self.code.lines().count()));
});
});
});
}
}