diff --git a/docs/changelog.md b/docs/changelog.md index 52ecb4fce..2629ba0a7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -22,6 +22,7 @@ As features stabilize some brief notes about them will accumulate here. * New [wezterm.gui.screens()](config/lua/wezterm.gui/screens.md) function for getting information about the available screens/monitors/displays * You may now use [wezterm.format](config/lua/wezterm/format.md) (or otherwise use strings with escape sequences) in the labels of the [Launcher Menu](config/launch.md#the-launcher-menu). * You may now specify `assume_emoji_presentation = true` (or `false`) in [wezterm.font()](config/lua/wezterm/font.md) and [wezterm.font_with_fallback()](config/lua/wezterm/font_with_fallback.md) +* Wayland: `zwp_text_input_v3` is now supported, which enables IME to work in wezterm if your compositor also implements this protocol. #### Fixed * [ActivateKeyTable](config/lua/keyassignment/ActivateKeyTable.md)'s `replace_current` field was not actually optional. Made it optional. [#2179](https://github.com/wez/wezterm/issues/2179) diff --git a/docs/config/lua/config/use_ime.md b/docs/config/lua/config/use_ime.md index 5c25e6ffa..8af94c17e 100644 --- a/docs/config/lua/config/use_ime.md +++ b/docs/config/lua/config/use_ime.md @@ -11,7 +11,7 @@ IME support is a platform dependent feature |Windows |Forever |Always enabled, cannot be disabled| |macOS |20200113-214446-bb6251f|defaults to enabled starting in 20220319-142410-0fcdea07. Earlier versions had problems with key repeat when enabled| |X11 |20211204-082213-a66c61ee9|[XIM](https://en.wikipedia.org/wiki/X_Input_Method) based. Your system needs to have a running input method engine (such as ibus or fcitx) that support the XIM protocol in order for wezterm to use it.| -|Wayland |Not yet|[#1772](https://github.com/wez/wezterm/issues/1772) is tracking IME support| +|Wayland |Nightly builds only|Your compositor must support `zwp_text_input_v3`| You can control whether the IME is enabled in your configuration file: diff --git a/window/src/os/wayland/connection.rs b/window/src/os/wayland/connection.rs index 2b4f1cd10..cbcc3e299 100644 --- a/window/src/os/wayland/connection.rs +++ b/window/src/os/wayland/connection.rs @@ -2,11 +2,12 @@ use super::pointer::*; use super::window::*; use crate::connection::ConnectionOps; +use crate::os::wayland::inputhandler::InputHandler; use crate::os::wayland::output::OutputHandler; use crate::os::x11::keyboard::Keyboard; use crate::screen::{ScreenInfo, Screens}; use crate::spawn::*; -use crate::{Connection, ScreenRect}; +use crate::{Connection, ScreenRect, WindowEvent}; use anyhow::{bail, Context}; use mio::unix::SourceFd; use mio::{Events, Interest, Poll, Token}; @@ -27,11 +28,19 @@ use wayland_client::{EventQueue, Main}; toolkit::default_environment!(MyEnvironment, desktop, fields=[ output_handler: OutputHandler, + input_handler: InputHandler, ], singles=[ wayland_protocols::wlr::unstable::output_management::v1::client::zwlr_output_manager_v1::ZwlrOutputManagerV1 => output_handler, + wayland_protocols::unstable::text_input::v3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3 => input_handler, ]); +impl MyEnvironment { + pub fn input_handler(&mut self) -> &mut InputHandler { + &mut self.input_handler + } +} + pub struct WaylandConnection { should_terminate: RefCell, pub(crate) next_window_id: AtomicUsize, @@ -61,7 +70,7 @@ pub struct WaylandConnection { pub(crate) key_repeat_delay: RefCell, pub(crate) last_serial: RefCell, seat_listener: SeatListener, - pub(crate) environment: RefCell>, + pub(crate) environment: Environment, event_q: RefCell, pub(crate) display: RefCell, } @@ -71,7 +80,10 @@ impl WaylandConnection { let (environment, display, event_q) = toolkit::new_default_environment!( MyEnvironment, desktop, - fields = [output_handler: OutputHandler::new()] + fields = [ + output_handler: OutputHandler::new(), + input_handler: InputHandler::new(), + ] )?; let mut pointer = None; @@ -95,6 +107,7 @@ impl WaylandConnection { log::error!("keyboard_event: {:#}", err); } }); + environment.with_inner(|env| env.input_handler.advise_seat(&seat, &keyboard)); seat_keyboards.insert(name, keyboard); } if has_ptr { @@ -111,6 +124,7 @@ impl WaylandConnection { let seat_listener; { + let env = environment.clone(); seat_listener = environment.listen_for_seats(move |seat, seat_data, _| { if seat_data.has_keyboard { if !seat_data.defunct { @@ -131,14 +145,18 @@ impl WaylandConnection { // up handling key events twice. if !seat_keyboards.contains_key(&seat_data.name) { let keyboard = seat.get_keyboard(); + keyboard.quick_assign(|keyboard, event, _| { let conn = Connection::get().unwrap().wayland(); if let Err(err) = conn.keyboard_event(keyboard, event) { log::error!("keyboard_event: {:#}", err); } }); + env.with_inner(|env| env.input_handler.advise_seat(&seat, &keyboard)); seat_keyboards.insert(seat_data.name.clone(), keyboard); } + } else { + env.with_inner(|env| env.input_handler.seat_defunct(&seat)); } } else { // If we previously had a keyboard object on this seat, it's no longer valid if @@ -158,7 +176,7 @@ impl WaylandConnection { Ok(Self { display: RefCell::new(display), - environment: RefCell::new(environment), + environment, should_terminate: RefCell::new(false), next_window_id: AtomicUsize::new(1), windows: RefCell::new(HashMap::new()), @@ -179,7 +197,7 @@ impl WaylandConnection { fn keyboard_event( &self, - _pointer: Main, + keyboard: Main, event: WlKeyboardEvent, ) -> anyhow::Result<()> { match &event { @@ -196,13 +214,30 @@ impl WaylandConnection { .get(&surface.as_ref().id()) { self.keyboard_window_id.borrow_mut().replace(window_id); + self.environment.with_inner(|env| { + if let Some(input) = + env.input_handler.get_text_input_for_keyboard(&keyboard) + { + input.enable(); + input.commit(); + } + env.input_handler.advise_surface(&surface, &keyboard); + }); } else { log::warn!("{:?}, no known surface", event); } } - WlKeyboardEvent::Leave { serial, .. } - | WlKeyboardEvent::Key { serial, .. } - | WlKeyboardEvent::Modifiers { serial, .. } => { + WlKeyboardEvent::Leave { serial, .. } => { + if let Some(input) = self + .environment + .with_inner(|env| env.input_handler.get_text_input_for_keyboard(&keyboard)) + { + input.disable(); + input.commit(); + } + *self.last_serial.borrow_mut() = *serial; + } + WlKeyboardEvent::Key { serial, .. } | WlKeyboardEvent::Modifiers { serial, .. } => { *self.last_serial.borrow_mut() = *serial; } WlKeyboardEvent::RepeatInfo { rate, delay } => { @@ -245,6 +280,15 @@ impl WaylandConnection { Ok(()) } + pub(crate) fn dispatch_to_focused_window(&self, event: WindowEvent) { + if let Some(&window_id) = self.keyboard_window_id.borrow().as_ref() { + if let Some(win) = self.window_by_id(window_id) { + let mut inner = win.borrow_mut(); + inner.events.dispatch(event); + } + } + } + pub(crate) fn next_window_id(&self) -> usize { self.next_window_id .fetch_add(1, ::std::sync::atomic::Ordering::Relaxed) @@ -380,7 +424,6 @@ impl ConnectionOps for WaylandConnection { fn screens(&self) -> anyhow::Result { if let Some(screens) = self .environment - .borrow() .with_inner(|env| env.output_handler.screens()) { return Ok(screens); @@ -388,7 +431,7 @@ impl ConnectionOps for WaylandConnection { let mut by_name = HashMap::new(); let mut virtual_rect: ScreenRect = euclid::rect(0, 0, 0, 0); - for output in self.environment.borrow().get_all_outputs() { + for output in self.environment.get_all_outputs() { toolkit::output::with_output_info(&output, |info| { let name = if info.name.is_empty() { format!("{} {}", info.model, info.make) diff --git a/window/src/os/wayland/copy_and_paste.rs b/window/src/os/wayland/copy_and_paste.rs index 37e21a1bb..7c0ab24ed 100644 --- a/window/src/os/wayland/copy_and_paste.rs +++ b/window/src/os/wayland/copy_and_paste.rs @@ -78,7 +78,6 @@ impl CopyAndPaste { let pointer = conn.pointer.borrow(); let primary_selection = if let Clipboard::PrimarySelection = clipboard { conn.environment - .borrow() .get_primary_selection_manager() .zip(pointer.primary_selection_device.as_ref()) } else { @@ -111,7 +110,6 @@ impl CopyAndPaste { None => { let source = conn .environment - .borrow() .require_global::() .create_data_source(); source.quick_assign(move |_source, event, _dispatch_data| { diff --git a/window/src/os/wayland/inputhandler.rs b/window/src/os/wayland/inputhandler.rs new file mode 100644 index 000000000..aab8df669 --- /dev/null +++ b/window/src/os/wayland/inputhandler.rs @@ -0,0 +1,165 @@ +//! Implements zwp_text_input_v3 for handling IME +use crate::connection::ConnectionOps; +use crate::os::wayland::{wl_id, WaylandConnection}; +use crate::{DeadKeyStatus, KeyCode, KeyEvent, Modifiers, WindowEvent}; +use smithay_client_toolkit::environment::GlobalHandler; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use wayland_client::protocol::wl_keyboard::WlKeyboard; +use wayland_client::protocol::wl_registry::WlRegistry; +use wayland_client::protocol::wl_seat::WlSeat; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::{Attached, DispatchData, Main}; +use wayland_protocols::unstable::text_input::v3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; +use wayland_protocols::unstable::text_input::v3::client::zwp_text_input_v3::{ + Event, ZwpTextInputV3, +}; + +#[derive(Debug, Default)] +struct Inner { + input_by_seat: HashMap>, + keyboard_to_seat: HashMap, + surface_to_keyboard: HashMap, +} + +impl Inner { + fn handle_event( + &mut self, + _input: Main, + event: Event, + _ddata: DispatchData, + _inner: &Arc>, + ) { + log::trace!("{event:?}"); + let conn = WaylandConnection::get().unwrap().wayland(); + match event { + Event::PreeditString { + text, + cursor_begin: _, + cursor_end: _, + } => { + conn.dispatch_to_focused_window(WindowEvent::AdviseDeadKeyStatus(match text { + Some(text) => DeadKeyStatus::Composing(text), + None => DeadKeyStatus::None, + })); + } + Event::CommitString { text } => { + conn.dispatch_to_focused_window(match text { + Some(text) => WindowEvent::KeyEvent(KeyEvent { + key: KeyCode::composed(&text), + modifiers: Modifiers::NONE, + repeat_count: 1, + key_is_down: true, + raw: None, + }), + None => WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::None), + }); + } + Event::Done { serial: _ } => { + conn.dispatch_to_focused_window(WindowEvent::AdviseDeadKeyStatus( + DeadKeyStatus::None, + )); + } + _ => {} + } + } +} + +pub struct InputHandler { + mgr: Option>, + inner: Arc>, +} + +impl InputHandler { + pub fn new() -> Self { + Self { + mgr: None, + inner: Arc::new(Mutex::new(Inner::default())), + } + } + + pub fn get_text_input_for_keyboard( + &self, + keyboard: &WlKeyboard, + ) -> Option> { + let inner = self.inner.lock().unwrap(); + let keyboard_id = wl_id(keyboard); + let seat_id = inner.keyboard_to_seat.get(&keyboard_id)?; + inner.input_by_seat.get(&seat_id).cloned() + } + + pub fn get_text_input_for_surface( + &self, + surface: &WlSurface, + ) -> Option> { + let inner = self.inner.lock().unwrap(); + let surface_id = wl_id(surface); + let keyboard_id = inner.surface_to_keyboard.get(&surface_id)?; + let seat_id = inner.keyboard_to_seat.get(&keyboard_id)?; + inner.input_by_seat.get(&seat_id).cloned() + } + + pub fn get_text_input_for_seat(&self, seat: &WlSeat) -> Option> { + let mgr = self.mgr.as_ref()?; + let mut inner = self.inner.lock().unwrap(); + let seat_id = wl_id(seat); + let input = inner.input_by_seat.entry(seat_id).or_insert_with(|| { + let input = mgr.get_text_input(seat); + let inner = Arc::clone(&self.inner); + + input.quick_assign(move |input, event, ddat| { + inner + .lock() + .unwrap() + .handle_event(input, event, ddat, &inner); + }); + + input.into() + }); + Some(input.clone()) + } + + pub fn advise_surface(&self, surface: &WlSurface, keyboard: &WlKeyboard) { + let surface_id = wl_id(surface); + let keyboard_id = wl_id(keyboard); + self.inner + .lock() + .unwrap() + .surface_to_keyboard + .insert(surface_id, keyboard_id); + } + + pub fn advise_seat(&self, seat: &WlSeat, keyboard: &WlKeyboard) { + self.get_text_input_for_seat(seat); + let keyboard_id = wl_id(keyboard); + let seat_id = wl_id(seat); + self.inner + .lock() + .unwrap() + .keyboard_to_seat + .insert(keyboard_id, seat_id); + } + + pub fn seat_defunct(&self, seat: &WlSeat) { + let seat_id = wl_id(seat); + self.inner.lock().unwrap().input_by_seat.remove(&seat_id); + } +} + +impl GlobalHandler for InputHandler { + fn created( + &mut self, + registry: Attached, + id: u32, + version: u32, + _ddata: DispatchData, + ) { + log::debug!("created ZwpTextInputV3 {id} {version}"); + let mgr = registry.bind::(1, id); + self.mgr.replace(mgr.into()); + } + + fn get(&self) -> std::option::Option> { + self.mgr.clone() + } +} diff --git a/window/src/os/wayland/mod.rs b/window/src/os/wayland/mod.rs index 60c5e81ee..a904110a6 100644 --- a/window/src/os/wayland/mod.rs +++ b/window/src/os/wayland/mod.rs @@ -1,6 +1,7 @@ #![cfg(all(unix, not(target_os = "macos")))] pub mod connection; +pub mod inputhandler; pub mod output; pub mod window; pub use self::window::*; @@ -10,3 +11,16 @@ mod copy_and_paste; mod drag_and_drop; mod frame; mod pointer; + +/// Returns the id of a wayland proxy object, suitable for using +/// a key into hash maps +pub fn wl_id(obj: T) -> u32 +where + I: wayland_client::Interface, + T: AsRef>, + I: AsRef>, + I: From>, +{ + let proxy: &wayland_client::Proxy = obj.as_ref(); + proxy.id() +} diff --git a/window/src/os/wayland/output.rs b/window/src/os/wayland/output.rs index 2aa0195e4..a78cf9857 100644 --- a/window/src/os/wayland/output.rs +++ b/window/src/os/wayland/output.rs @@ -1,5 +1,6 @@ //! Dealing with Wayland outputs +use crate::os::wayland::wl_id; use crate::screen::{ScreenInfo, Screens}; use crate::ScreenRect; use smithay_client_toolkit::environment::GlobalHandler; @@ -7,7 +8,7 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; use wayland_client::protocol::wl_output::Transform; use wayland_client::protocol::wl_registry::WlRegistry; -use wayland_client::{Attached, DispatchData, Interface, Main, Proxy}; +use wayland_client::{Attached, DispatchData, Main}; use wayland_protocols::wlr::unstable::output_management::v1::client::zwlr_output_head_v1::{ Event as ZwlrOutputHeadEvent, ZwlrOutputHeadV1, }; @@ -55,17 +56,6 @@ struct Inner { zwlr_head_info: HashMap, } -fn wl_id(obj: T) -> u32 -where - I: Interface, - T: AsRef>, - I: AsRef>, - I: From>, -{ - let proxy: &Proxy = obj.as_ref(); - proxy.id() -} - impl Inner { fn handle_zwlr_mode_event( &mut self, diff --git a/window/src/os/wayland/window.rs b/window/src/os/wayland/window.rs index 6f7e3e8c8..4a1f57820 100644 --- a/window/src/os/wayland/window.rs +++ b/window/src/os/wayland/window.rs @@ -3,9 +3,10 @@ use super::frame::{ConceptConfig, ConceptFrame}; use super::pointer::*; use crate::connection::ConnectionOps; use crate::os::wayland::connection::WaylandConnection; +use crate::os::wayland::wl_id; use crate::os::x11::keyboard::Keyboard; use crate::{ - Clipboard, Connection, Dimensions, MouseCursor, Point, RequestedWindowGeometry, + Clipboard, Connection, Dimensions, MouseCursor, Point, Rect, RequestedWindowGeometry, ResolvedGeometry, ScreenPoint, Window, WindowEvent, WindowEventSender, WindowKeyEvent, WindowOps, WindowState, }; @@ -124,7 +125,7 @@ impl KeyRepeatState { pub struct WaylandWindowInner { window_id: usize, - events: WindowEventSender, + pub(crate) events: WindowEventSender, surface: Attached, surface_factor: i32, copy_and_paste: Arc>, @@ -144,6 +145,7 @@ pub struct WaylandWindowInner { frame_callback: Option>, invalidated: bool, font_config: Rc, + text_cursor: Option, config: Option, // cache the title for comparison to avoid spamming // the compositor with updates that don't actually change it @@ -254,24 +256,21 @@ impl WaylandWindow { let (pending_first_configure, wait_configure) = async_channel::bounded(1); - let surface = conn - .environment - .borrow_mut() - .create_surface_with_scale_callback({ - let pending_event = Arc::clone(&pending_event); - move |dpi, surface, _dispatch_data| { - pending_event.lock().unwrap().dpi.replace(dpi); - log::debug!( - "surface id={} dpi scale changed to {}", - surface.as_ref().id(), - dpi - ); - WaylandConnection::with_window_inner(window_id, move |inner| { - inner.dispatch_pending_event(); - Ok(()) - }); - } - }); + let surface = conn.environment.create_surface_with_scale_callback({ + let pending_event = Arc::clone(&pending_event); + move |dpi, surface, _dispatch_data| { + pending_event.lock().unwrap().dpi.replace(dpi); + log::debug!( + "surface id={} dpi scale changed to {}", + surface.as_ref().id(), + dpi + ); + WaylandConnection::with_window_inner(window_id, move |inner| { + inner.dispatch_pending_event(); + Ok(()) + }); + } + }); conn.surface_to_window_id .borrow_mut() .insert(surface.as_ref().id(), window_id); @@ -293,7 +292,6 @@ impl WaylandWindow { let mut window = conn .environment - .borrow() .create_window::( surface.clone().detach(), theme_manager, @@ -373,6 +371,7 @@ impl WaylandWindow { title: None, gl_state: None, wegl_surface: None, + text_cursor: None, })); let window_handle = Window::Wayland(WaylandWindow(window_id)); @@ -461,6 +460,7 @@ impl WaylandWindowInner { mapper.update_modifier_state(0, 0, 0, 0); self.key_repeat.take(); self.events.dispatch(WindowEvent::FocusChanged(focused)); + self.text_cursor.take(); } pub(crate) fn dispatch_dropped_files(&mut self, paths: Vec) { @@ -897,6 +897,13 @@ impl WindowOps for WaylandWindow { }); } + fn set_text_cursor_position(&self, cursor: Rect) { + WaylandConnection::with_window_inner(self.0, move |inner| { + inner.set_text_cursor_position(cursor); + Ok(()) + }); + } + fn set_title(&self, title: &str) { let title = title.to_owned(); WaylandConnection::with_window_inner(self.0, move |inner| { @@ -1113,6 +1120,31 @@ impl WaylandWindowInner { } } + fn set_text_cursor_position(&mut self, rect: Rect) { + let surface_id = wl_id(&*self.surface); + let conn = Connection::get().unwrap().wayland(); + if surface_id == *conn.active_surface_id.borrow() { + if self.text_cursor.map(|prior| prior != rect).unwrap_or(true) { + self.text_cursor.replace(rect); + + conn.environment.with_inner(|env| { + if let Some(input) = env + .input_handler() + .get_text_input_for_surface(&self.surface) + { + input.set_cursor_rectangle( + rect.min_x() as i32, + rect.min_y() as i32, + rect.width() as i32, + rect.height() as i32, + ); + input.commit(); + } + }); + } + } + } + /// Change the title for the window manager fn set_title(&mut self, title: String) { if let Some(last_title) = self.title.as_ref() { diff --git a/window/src/os/x_and_wayland.rs b/window/src/os/x_and_wayland.rs index 1a090f50c..d61a84334 100644 --- a/window/src/os/x_and_wayland.rs +++ b/window/src/os/x_and_wayland.rs @@ -344,7 +344,7 @@ impl WindowOps for Window { match self { Self::X11(x) => x.set_text_cursor_position(cursor), #[cfg(feature = "wayland")] - Self::Wayland(_) => {} + Self::Wayland(w) => w.set_text_cursor_position(cursor), } }