From eb1756df3fc4e53b82947c4333f68259da6b9c09 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 16 Feb 2026 03:36:31 -0500 Subject: [PATCH] ibus wayland fix --- Cargo.lock | 9 ++ crates/egui-winit/src/lib.rs | 168 +++++++++++++++++++--- examples/text_input_test/Cargo.toml | 14 ++ examples/text_input_test/src/main.rs | 205 +++++++++++++++++++++++++++ 4 files changed, 378 insertions(+), 18 deletions(-) create mode 100644 examples/text_input_test/Cargo.toml create mode 100644 examples/text_input_test/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 61bf459b..f55018fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index ed293e39..b180b244 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -106,6 +106,14 @@ pub struct State { allow_ime: bool, ime_rect_px: Option, + + /// 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 + 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 + 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 + 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,23 +977,28 @@ impl State { } if let Some(ime) = ime { - 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) - || self.egui_ctx.input(|i| !i.events.is_empty()) - { - self.ime_rect_px = Some(ime_rect_px); - profiling::scope!("set_ime_cursor_area"); - window.set_ime_cursor_area( - winit::dpi::PhysicalPosition { - x: ime_rect_px.min.x, - y: ime_rect_px.min.y, - }, - winit::dpi::PhysicalSize { - width: ime_rect_px.width(), - height: ime_rect_px.height(), - }, - ); + // 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) + || self.egui_ctx.input(|i| !i.events.is_empty()) + { + self.ime_rect_px = Some(ime_rect_px); + profiling::scope!("set_ime_cursor_area"); + window.set_ime_cursor_area( + winit::dpi::PhysicalPosition { + x: ime_rect_px.min.x, + y: ime_rect_px.min.y, + }, + winit::dpi::PhysicalSize { + width: ime_rect_px.width(), + height: ime_rect_px.height(), + }, + ); + } } } 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 +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'. diff --git a/examples/text_input_test/Cargo.toml b/examples/text_input_test/Cargo.toml new file mode 100644 index 00000000..edddc424 --- /dev/null +++ b/examples/text_input_test/Cargo.toml @@ -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"] } diff --git a/examples/text_input_test/src/main.rs b/examples/text_input_test/src/main.rs new file mode 100644 index 00000000..011c2bae --- /dev/null +++ b/examples/text_input_test/src/main.rs @@ -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::::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, +} + +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())); + }); + }); + }); + } +}