diff --git a/Cargo.toml b/Cargo.toml index 381eddf5..a4e37d31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ linearize = { version = "0.1.3", features = ["derive"] } png = "0.18.0" rustc-demangle = { version = "0.1.24", optional = true } tracy-client-sys = { version = "0.24.1", features = ["ondemand", "manual-lifetime", "debuginfod", "demangle"], optional = true } -kbvm = "0.1.5" +kbvm = { version = "0.1.5", features = ["compose"] } tiny-skia = { version = "0.11.4", default-features = false, features = ["std"] } regex = "1.11.1" cfg-if = "1.0.0" diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 596d5dcc..e54758fc 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -52,7 +52,8 @@ use { pointer_owner::PointerOwnerHolder, tablet::TabletSeatData, text_input::{ - InputMethod, InputMethodKeyboardGrab, zwp_text_input_v3::ZwpTextInputV3, + InputMethod, InputMethodKeyboardGrab, simple_im::SimpleIm, + zwp_text_input_v3::ZwpTextInputV3, }, touch_owner::TouchOwnerHolder, wl_keyboard::{REPEAT_INFO_SINCE, WlKeyboard, WlKeyboardError}, @@ -237,6 +238,7 @@ pub struct WlSeatGlobal { marks: CopyHashMap>, modifiers_listener: EventListener, modifiers_forward: EventSource, + simple_im: CloneCell>>, } #[derive(Copy, Clone)] @@ -258,6 +260,7 @@ impl WlSeatGlobal { let cursor_user_group = CursorUserGroup::create(state); let cursor_user = cursor_user_group.create_user(); cursor_user.activate(); + let simple_im = SimpleIm::new(&state.kb_ctx.ctx); let slf = Rc::new_cyclic(|slf: &Weak| Self { id: state.seat_ids.next(), name, @@ -305,7 +308,7 @@ impl WlSeatGlobal { data_control_devices: Default::default(), text_inputs: Default::default(), text_input: Default::default(), - input_method: Default::default(), + input_method: CloneCell::new(simple_im.clone().map(|im| im as _)), input_method_grab: Default::default(), forward: Cell::new(false), focus_follows_mouse: Cell::new(true), @@ -326,6 +329,7 @@ impl WlSeatGlobal { marks: Default::default(), modifiers_listener: EventListener::new(slf.clone()), modifiers_forward: Default::default(), + simple_im: CloneCell::new(simple_im), }); slf.pointer_cursor.set_owner(slf.clone()); slf.modifiers_listener diff --git a/src/ifs/wl_seat/text_input.rs b/src/ifs/wl_seat/text_input.rs index b8855295..3802afa6 100644 --- a/src/ifs/wl_seat/text_input.rs +++ b/src/ifs/wl_seat/text_input.rs @@ -12,6 +12,7 @@ use { std::rc::Rc, }; +pub mod simple_im; pub mod zwp_input_method_keyboard_grab_v2; pub mod zwp_input_method_manager_v2; pub mod zwp_input_method_v2; @@ -36,6 +37,8 @@ pub trait InputMethod { fn text_change_cause(&self, cause: u32); fn surrounding_text(&self, text: &str, cursor: u32, anchor: u32); fn done(self: Rc, seat: &WlSeatGlobal); + fn is_simple(&self) -> bool; + fn cancel_simple(&self, seat: &WlSeatGlobal); } pub trait InputMethodKeyboardGrab { @@ -58,6 +61,32 @@ pub enum TextDisconnectReason { } impl WlSeatGlobal { + fn can_set_new_im(&self) -> bool { + match self.input_method.get() { + None => true, + Some(im) => im.is_simple(), + } + } + + fn cannot_set_new_im(&self) -> bool { + !self.can_set_new_im() + } + + fn set_input_method(self: &Rc, im: Rc) { + if let Some(old) = self.input_method.take() { + old.cancel_simple(self); + } + self.input_method.set(Some(im)); + self.create_text_input_connection(TextConnectReason::InputMethodCreated); + } + + fn remove_input_method(self: &Rc) { + self.input_method.take(); + if let Some(im) = self.simple_im.get() { + self.set_input_method(im); + } + } + fn create_text_input_connection(self: &Rc, text_connect_reason: TextConnectReason) { let Some(im) = self.input_method.get() else { return; diff --git a/src/ifs/wl_seat/text_input/simple_im.rs b/src/ifs/wl_seat/text_input/simple_im.rs new file mode 100644 index 00000000..589d2c11 --- /dev/null +++ b/src/ifs/wl_seat/text_input/simple_im.rs @@ -0,0 +1,204 @@ +use { + crate::{ + backend::KeyState, + ifs::{ + wl_seat::{ + WlSeatGlobal, + text_input::{ + InputMethod, InputMethodKeyboardGrab, TextDisconnectReason, TextInputConnection, + }, + }, + wl_surface::zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2, + }, + keyboard::KeyboardState, + utils::{clonecell::CloneCell, smallmap::SmallMap}, + wire::ZwpInputPopupSurfaceV2Id, + }, + kbvm::{ + Keycode, ModifierMask, syms, + xkb::{ + self, + compose::{self, FeedResult}, + diagnostic::WriteToLog, + }, + }, + std::{ + cell::{Cell, RefCell}, + rc::Rc, + }, +}; + +pub struct SimpleIm { + con: CloneCell>>, + popups: SmallMap, 1>, + active: Cell, + activate: Cell>, + table: compose::ComposeTable, + initial_state: compose::State, + states: RefCell>, +} + +struct State { + state: compose::State, + char: char, +} + +impl SimpleIm { + pub fn new(ctx: &xkb::Context) -> Option> { + let table = ctx.compose_table_builder().build(WriteToLog)?; + Some(Rc::new(Self { + con: Default::default(), + popups: Default::default(), + active: Default::default(), + activate: Default::default(), + states: Default::default(), + initial_state: table.create_state(), + table, + })) + } +} + +impl InputMethod for SimpleIm { + fn set_connection(&self, con: Option<&Rc>) { + self.con.set(con.cloned()); + } + + fn popups(&self) -> &SmallMap, 1> { + &self.popups + } + + fn activate(&self) { + self.activate.set(Some(true)); + } + + fn deactivate(&self) { + self.activate.set(Some(false)); + } + + fn content_type(&self, _hint: u32, _purpose: u32) { + // nothing + } + + fn text_change_cause(&self, _cause: u32) { + // nothing + } + + fn surrounding_text(&self, _text: &str, _cursor: u32, _anchor: u32) { + // nothing + } + + fn done(self: Rc, seat: &WlSeatGlobal) { + let Some(active) = self.activate.take() else { + return; + }; + self.active.set(active); + if active { + self.states.borrow_mut().clear(); + seat.input_method_grab.set(Some(self)); + } else { + seat.input_method_grab.take(); + } + } + + fn is_simple(&self) -> bool { + true + } + + fn cancel_simple(&self, seat: &WlSeatGlobal) { + seat.input_method_grab.take(); + if let Some(con) = self.con.get() { + con.disconnect(TextDisconnectReason::InputMethodDestroyed); + } + } +} + +impl InputMethodKeyboardGrab for SimpleIm { + fn on_key(&self, _time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) -> bool { + if state != KeyState::Pressed { + return true; + } + let Some(con) = self.con.get() else { + return true; + }; + let mut buf = [0; 4]; + let mut forward_to_node = true; + let states = &mut *self.states.borrow_mut(); + let lookup = kb_state.map.lookup_table.lookup( + kb_state.mods.group, + kb_state.mods.mods, + Keycode::from_evdev(key), + ); + let mods = lookup.remaining_mods(); + let is_control = mods.contains(ModifierMask::CONTROL); + for sym in lookup { + let sym = sym.keysym(); + let mut new_state = states + .last() + .map(|s| s.state.clone()) + .unwrap_or_else(|| self.initial_state.clone()); + let Some(fr) = self.table.feed(&mut new_state, sym) else { + continue; + }; + forward_to_node = false; + let mut send_preedit = |char: char| { + let s = char.encode_utf8(&mut buf); + let len = s.len() as i32; + con.text_input.send_preedit_string(Some(s), len, len); + }; + match fr { + FeedResult::Pending => { + let char = sym.char().unwrap_or('ยท'); + states.push(State { + state: new_state, + char, + }); + send_preedit(char); + con.text_input.send_done(); + } + FeedResult::Aborted + if sym == syms::Escape || (matches!(sym, syms::c | syms::w) && is_control) => + { + states.clear(); + con.text_input.send_preedit_string(None, 0, 0); + con.text_input.send_done(); + } + FeedResult::Aborted if sym == syms::BackSpace => { + states.pop(); + if let Some(state) = states.last() { + send_preedit(state.char); + } else { + con.text_input.send_preedit_string(None, 0, 0); + } + con.text_input.send_done(); + } + FeedResult::Aborted => { + // nothing + } + FeedResult::Composed { string, keysym } => { + states.clear(); + let s = if string.is_some() { + string + } else if let Some(sym) = keysym + && let Some(char) = sym.char() + { + Some(char.encode_utf8(&mut buf) as &str) + } else { + None + }; + con.text_input.send_preedit_string(None, 0, 0); + con.text_input.send_commit_string(s); + con.text_input.send_done(); + } + } + } + forward_to_node + } + + fn on_modifiers(&self, _kb_state: &KeyboardState) -> bool { + true + } + + fn on_repeat_info(&self) { + // nothing + } +} diff --git a/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs b/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs index d78dbc7b..c848fb23 100644 --- a/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs +++ b/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs @@ -2,7 +2,7 @@ use { crate::{ client::{CAP_INPUT_METHOD, Client, ClientCaps, ClientError}, globals::{Global, GlobalName}, - ifs::wl_seat::text_input::{TextConnectReason, zwp_input_method_v2::ZwpInputMethodV2}, + ifs::wl_seat::text_input::zwp_input_method_v2::ZwpInputMethodV2, leaks::Tracker, object::{Object, Version}, wire::{ZwpInputMethodManagerV2Id, zwp_input_method_manager_v2::*}, @@ -72,7 +72,7 @@ impl ZwpInputMethodManagerV2RequestHandler for ZwpInputMethodManagerV2 { fn get_input_method(&self, req: GetInputMethod, _slf: &Rc) -> Result<(), Self::Error> { let seat = self.client.lookup(req.seat)?; - let inert = seat.global.input_method.is_some(); + let inert = seat.global.cannot_set_new_im(); let im = Rc::new(ZwpInputMethodV2 { id: req.input_method, client: self.client.clone(), @@ -90,9 +90,7 @@ impl ZwpInputMethodManagerV2RequestHandler for ZwpInputMethodManagerV2 { if inert { im.send_unavailable(); } else { - seat.global.input_method.set(Some(im)); - seat.global - .create_text_input_connection(TextConnectReason::InputMethodCreated); + seat.global.set_input_method(im); } Ok(()) } diff --git a/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs b/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs index 5f6e3b15..fe82d42a 100644 --- a/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs +++ b/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs @@ -53,7 +53,7 @@ impl ZwpInputMethodV2 { } self.popups.clear(); if !self.inert { - self.seat.input_method.take(); + self.seat.remove_input_method(); } } @@ -136,6 +136,14 @@ impl InputMethod for ZwpInputMethodV2 { fn done(self: Rc, _seat: &WlSeatGlobal) { (*self).send_done(); } + + fn is_simple(&self) -> bool { + false + } + + fn cancel_simple(&self, _seat: &WlSeatGlobal) { + unreachable!(); + } } impl ZwpInputMethodV2RequestHandler for ZwpInputMethodV2 {