ibus wayland fix
This commit is contained in:
parent
978ec6c870
commit
eb1756df3f
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'.
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
@ -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()));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue