Lightningbeam/lightningbeam-ui/lightningbeam-editor/src/widgets/text_field.rs

266 lines
8.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Custom text field widget with IME workaround
//!
//! WORKAROUND for IBus Wayland bug (egui issue #7485):
//! https://github.com/emilk/egui/issues/7485
//!
//! IBus on Wayland only delivers one character through normal TextEdit handling.
//! This widget renders a custom text field and handles all input manually.
//!
//! TODO: Remove this workaround once the upstream issue is fixed.
use eframe::egui;
/// Convert egui Key to character for manual text input handling.
/// Uses egui's name()/symbol_or_name() where possible, with shift handling
/// for uppercase letters and shifted symbols (US keyboard layout).
fn key_to_char(key: egui::Key, shift: bool) -> Option<char> {
let symbol = key.symbol_or_name();
// If it's a single character, we can use it (with shift handling)
if symbol.chars().count() == 1 {
let c = symbol.chars().next().unwrap();
// Handle letters - apply shift for case
if c.is_ascii_alphabetic() {
return Some(if shift { c.to_ascii_uppercase() } else { c.to_ascii_lowercase() });
}
// Handle digits with shift -> symbols (US keyboard layout)
if c.is_ascii_digit() && shift {
return Some(match c {
'0' => ')',
'1' => '!',
'2' => '@',
'3' => '#',
'4' => '$',
'5' => '%',
'6' => '^',
'7' => '&',
'8' => '*',
'9' => '(',
_ => c,
});
}
// Handle punctuation with shift (US keyboard layout)
if shift {
return Some(match c {
'-' | '' => '_', // Minus (egui uses special minus char)
'=' => '+',
'[' => '{',
']' => '}',
'\\' => '|',
';' => ':',
'\'' => '"',
',' => '<',
'.' => '>',
'/' => '?',
'`' => '~',
_ => c,
});
}
return Some(c);
}
// Special case: Space returns "Space" not " "
if matches!(key, egui::Key::Space) {
return Some(' ');
}
None // Non-printable keys (arrows, function keys, etc.)
}
/// Response from the IME text field widget
#[allow(dead_code)] // Standard widget response fields; callers will use as features expand
pub struct ImeTextFieldResponse {
/// The egui response for the text field area
pub response: egui::Response,
/// Whether the text was changed
pub changed: bool,
/// Whether Enter was pressed (for single-line fields)
pub submitted: bool,
/// Whether Escape was pressed
pub cancelled: bool,
}
/// A text field widget that works around IBus Wayland IME issues.
///
/// This is a temporary workaround for egui issue #7485. Use this instead of
/// `egui::TextEdit` when you need text input to work on Wayland with IBus.
pub struct ImeTextField<'a> {
text: &'a mut String,
placeholder: Option<&'a str>,
font_size: f32,
desired_width: Option<f32>,
request_focus: bool,
}
impl<'a> ImeTextField<'a> {
/// Create a new text field widget
pub fn new(text: &'a mut String) -> Self {
Self {
text,
placeholder: None,
font_size: 14.0,
desired_width: None,
request_focus: false,
}
}
/// Set placeholder text shown when the field is empty and unfocused
pub fn placeholder(mut self, placeholder: &'a str) -> Self {
self.placeholder = Some(placeholder);
self
}
/// Set the font size (default: 14.0)
pub fn font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
/// Set the desired width of the field
pub fn desired_width(mut self, width: f32) -> Self {
self.desired_width = Some(width);
self
}
/// Request focus on this field
pub fn request_focus(mut self) -> Self {
self.request_focus = true;
self
}
/// Show the text field widget
pub fn show(self, ui: &mut egui::Ui) -> ImeTextFieldResponse {
let desired_size = egui::vec2(
self.desired_width.unwrap_or(ui.available_width()),
self.font_size + 8.0,
);
let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
let id = response.id;
// Handle click to focus
if response.clicked() {
ui.memory_mut(|m| m.request_focus(id));
}
// Handle focus request
if self.request_focus {
ui.memory_mut(|m| m.request_focus(id));
}
let has_focus = ui.memory(|m| m.has_focus(id));
// Draw the text field background
let bg_color = if has_focus {
egui::Color32::from_rgb(50, 50, 55)
} else {
egui::Color32::from_rgb(40, 40, 45)
};
let stroke = if has_focus {
egui::Stroke::new(1.0, egui::Color32::from_rgb(100, 150, 255))
} else {
egui::Stroke::new(1.0, egui::Color32::from_rgb(60, 60, 65))
};
ui.painter().rect(rect, 3.0, bg_color, stroke, egui::StrokeKind::Middle);
// Draw the text or placeholder
let text_pos = rect.min + egui::vec2(6.0, (rect.height() - self.font_size) / 2.0);
if self.text.is_empty() && !has_focus {
if let Some(placeholder) = self.placeholder {
ui.painter().text(
text_pos,
egui::Align2::LEFT_TOP,
placeholder,
egui::FontId::proportional(self.font_size),
egui::Color32::from_gray(100),
);
}
} else {
ui.painter().text(
text_pos,
egui::Align2::LEFT_TOP,
self.text.as_str(),
egui::FontId::proportional(self.font_size),
egui::Color32::from_gray(220),
);
// Draw cursor when focused
if has_focus {
let text_width = ui.painter().layout_no_wrap(
self.text.clone(),
egui::FontId::proportional(self.font_size),
egui::Color32::WHITE,
).rect.width();
let cursor_x = text_pos.x + text_width + 1.0;
let blink = (ui.input(|i| i.time) * 2.0).fract() < 0.5;
if blink {
ui.painter().line_segment(
[
egui::pos2(cursor_x, rect.min.y + 4.0),
egui::pos2(cursor_x, rect.max.y - 4.0),
],
egui::Stroke::new(1.0, egui::Color32::WHITE),
);
}
ui.ctx().request_repaint(); // For cursor blinking
}
}
// Handle keyboard input when focused
let mut changed = false;
let mut submitted = false;
let mut cancelled = false;
if has_focus {
ui.input(|i| {
for event in &i.events {
if let egui::Event::Key { key, pressed: true, modifiers, .. } = event {
// Skip if modifier keys are held (except shift)
if modifiers.ctrl || modifiers.alt || modifiers.command {
continue;
}
match key {
egui::Key::Backspace => {
if !self.text.is_empty() {
self.text.pop();
changed = true;
}
}
egui::Key::Enter => {
submitted = true;
}
egui::Key::Escape => {
cancelled = true;
}
_ => {
if let Some(c) = key_to_char(*key, modifiers.shift) {
self.text.push(c);
changed = true;
}
}
}
}
}
});
// Lose focus on Escape
if cancelled {
ui.memory_mut(|m| m.surrender_focus(id));
}
}
ImeTextFieldResponse {
response,
changed,
submitted,
cancelled,
}
}
}