use std::{
cell::{Cell, RefCell},
rc::Weak,
};
use gpui::{Bounds, Pixels};
use gtk::{gdk, glib, prelude::*};
use tauri::Wry;
use super::{ImeEvent, KeyRoute, pixel_rect, route_key, utf16_offset};
use crate::{platform::window::WindowInner, runtime};
pub(crate) struct Ime {
window: Weak<WindowInner>,
context: gtk::IMMulticontext,
text_input: Cell<bool>,
active: Cell<bool>,
focused: Cell<bool>,
composing: Cell<bool>,
filtering: Cell<bool>,
captured: RefCell<Vec<ImeEvent>>,
queued: RefCell<Vec<ImeEvent>>,
claimed_keys: RefCell<Vec<u16>>,
}
impl Ime {
pub fn new(window: &tauri::Window<Wry>, inner: Weak<WindowInner>) -> Option<Self> {
let widget = window
.gtk_window()
.inspect_err(|error| log::warn!("IME: no GTK window: {error}"))
.ok()?;
let Some(client) = widget.window() else {
log::warn!("IME: the GTK window is not realized");
return None;
};
let context = gtk::IMMulticontext::new();
context.set_client_window(Some(&client));
context.set_use_preedit(true);
context.connect_commit({
let inner = inner.clone();
move |_, text| emit(&inner, ImeEvent::Commit(text.to_owned()))
});
context.connect_preedit_changed({
let inner = inner.clone();
move |context| {
let (text, _, cursor) = context.preedit_string();
let cursor = utf16_offset(&text, cursor.max(0) as usize);
emit(
&inner,
ImeEvent::Preedit {
text: text.into(),
cursor,
},
);
}
});
context.connect_preedit_end({
let inner = inner.clone();
move |_| emit(&inner, ImeEvent::End)
});
let _ = widget.connect_event({
let inner = inner.clone();
move |_, event| key_event(&inner, event)
});
Some(Self {
window: inner,
context,
text_input: Cell::new(false),
active: Cell::new(window.is_focused().unwrap_or(false)),
focused: Cell::new(false),
composing: Cell::new(false),
filtering: Cell::new(false),
captured: RefCell::default(),
queued: RefCell::default(),
claimed_keys: RefCell::default(),
})
}
pub fn set_text_input(&self, focused: bool) {
self.text_input.set(focused);
if !focused {
self.composing.set(false);
self.queued.borrow_mut().clear();
}
self.focus_later();
}
pub fn set_active(&self, active: bool) {
self.active.set(active);
self.focus_later();
}
fn focus_later(&self) {
let Some(inner) = self.window.upgrade() else {
return;
};
let window = self.window.clone();
(inner.defer)(Box::new(move || {
if let Some(inner) = window.upgrade()
&& let Some(ime) = &inner.ime
{
ime.sync_focus();
}
}));
}
fn sync_focus(&self) {
let focused = self.text_input.get() && self.active.get();
if self.focused.replace(focused) == focused {
return;
}
if focused {
self.context.focus_in();
} else {
self.context.focus_out();
self.claimed_keys.borrow_mut().clear();
}
}
pub fn set_cursor(&self, bounds: Bounds<Pixels>) {
let [left, top, right, bottom] = pixel_rect(bounds, 1.);
self.context.set_cursor_location(&gdk::Rectangle::new(
left,
top,
right - left,
bottom - top,
));
}
pub fn flush(&self, inner: &WindowInner) {
let events = std::mem::take(&mut *self.queued.borrow_mut());
self.apply(inner, events);
}
pub fn shutdown(&self) {
if self.focused.replace(false) {
self.context.focus_out();
}
self.text_input.set(false);
self.queued.borrow_mut().clear();
self.context.set_client_window(None::<&gdk::Window>);
}
fn key(&self, inner: &WindowInner, key: &gdk::EventKey, pressed: bool) -> glib::Propagation {
let composing = self.composing.get();
self.filtering.set(true);
let consumed = self.context.filter_keypress(key);
self.filtering.set(false);
let events = std::mem::take(&mut *self.captured.borrow_mut());
let code = key.hardware_keycode();
let stop = if key.is_modifier() {
self.apply(inner, events);
false
} else if !pressed {
let mut claimed = self.claimed_keys.borrow_mut();
let position = claimed.iter().position(|&c| c == code);
if let Some(position) = position {
claimed.swap_remove(position);
}
drop(claimed);
self.apply(inner, events);
position.is_some()
} else {
match route_key(consumed, &events, key.keyval().to_unicode(), composing) {
KeyRoute::Key => {
self.apply(inner, events);
false
}
KeyRoute::KeyInsteadOfCommit => false,
KeyRoute::Ime => {
self.claimed_keys.borrow_mut().push(code);
self.apply(inner, events);
true
}
}
};
if stop {
glib::Propagation::Stop
} else {
glib::Propagation::Proceed
}
}
fn apply(&self, inner: &WindowInner, events: Vec<ImeEvent>) {
if events.is_empty() || !self.text_input.get() {
return;
}
let mut cursor = None;
for event in events {
match event {
ImeEvent::Commit(text) => {
self.composing.set(false);
inner.insert_text(&text);
}
ImeEvent::Preedit { text, cursor: at } if !text.is_empty() => {
self.composing.set(true);
cursor = inner
.input_handler(|handler| {
handler.replace_and_mark_text_in_range(None, &text, Some(at..at));
handler.ime_candidate_bounds()
})
.flatten();
}
ImeEvent::Preedit { .. } | ImeEvent::End => {
let remove = matches!(event, ImeEvent::Preedit { .. });
self.composing.set(false);
inner.input_handler(|handler| {
if remove && let Some(marked) = handler.marked_text_range() {
handler.replace_text_in_range(Some(marked), "");
}
handler.unmark_text();
});
}
}
}
if let Some(bounds) = cursor {
self.set_cursor(bounds);
}
}
}
fn emit(inner: &Weak<WindowInner>, event: ImeEvent) {
let Some(inner) = inner.upgrade() else { return };
let Some(ime) = &inner.ime else { return };
if ime.filtering.get() {
ime.captured.borrow_mut().push(event);
return;
}
if !ime.text_input.get() {
return;
}
ime.queued.borrow_mut().push(event);
if runtime::input(|| ime.flush(&inner)).is_none() {
inner.waker.wake();
}
}
fn key_event(inner: &Weak<WindowInner>, event: &gdk::Event) -> glib::Propagation {
let pressed = match event.event_type() {
gdk::EventType::KeyPress => true,
gdk::EventType::KeyRelease => false,
_ => return glib::Propagation::Proceed,
};
let Some(inner) = inner.upgrade() else {
return glib::Propagation::Proceed;
};
let (Some(ime), Some(key)) = (&inner.ime, event.downcast_ref::<gdk::EventKey>()) else {
return glib::Propagation::Proceed;
};
if !ime.focused.get() {
return glib::Propagation::Proceed;
}
runtime::input(|| ime.key(&inner, key, pressed)).unwrap_or(glib::Propagation::Proceed)
}