Wayland: Implement text_input_v3 and xkb compose (#11712)

Release Notes:

- N/A

Fixes #9207 
Known Issues:
- [ ] ~~After launching Zed and immediately trying to change input
method, the input panel will appear at Point{0, 0}~~
- [ ] ~~`ime_handle_preedit` should not trigger `write_to_primary`~~
Move to other PR
- [ ] ~~Cursor is visually stuck at the end.~~ Move to other PR
Currently tested with KDE & fcitx5.
This commit is contained in:
Fernando Tagawa 2024-05-16 15:42:43 -03:00 committed by GitHub
parent fdadbc7174
commit 5596a34311
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 281 additions and 5 deletions

View file

@ -655,6 +655,68 @@ impl Keystroke {
ime_key,
}
}
/**
* Returns which symbol the dead key represents
* https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux
*/
pub fn underlying_dead_key(keysym: Keysym) -> Option<String> {
match keysym {
Keysym::dead_grave => Some("`".to_owned()),
Keysym::dead_acute => Some("´".to_owned()),
Keysym::dead_circumflex => Some("^".to_owned()),
Keysym::dead_tilde => Some("~".to_owned()),
Keysym::dead_perispomeni => Some("͂".to_owned()),
Keysym::dead_macron => Some("¯".to_owned()),
Keysym::dead_breve => Some("˘".to_owned()),
Keysym::dead_abovedot => Some("˙".to_owned()),
Keysym::dead_diaeresis => Some("¨".to_owned()),
Keysym::dead_abovering => Some("˚".to_owned()),
Keysym::dead_doubleacute => Some("˝".to_owned()),
Keysym::dead_caron => Some("ˇ".to_owned()),
Keysym::dead_cedilla => Some("¸".to_owned()),
Keysym::dead_ogonek => Some("˛".to_owned()),
Keysym::dead_iota => Some("ͅ".to_owned()),
Keysym::dead_voiced_sound => Some("".to_owned()),
Keysym::dead_semivoiced_sound => Some("".to_owned()),
Keysym::dead_belowdot => Some("̣̣".to_owned()),
Keysym::dead_hook => Some("̡".to_owned()),
Keysym::dead_horn => Some("̛".to_owned()),
Keysym::dead_stroke => Some("̶̶".to_owned()),
Keysym::dead_abovecomma => Some("̓̓".to_owned()),
Keysym::dead_psili => Some("᾿".to_owned()),
Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
Keysym::dead_dasia => Some("".to_owned()),
Keysym::dead_doublegrave => Some("̏".to_owned()),
Keysym::dead_belowring => Some("˳".to_owned()),
Keysym::dead_belowmacron => Some("̱".to_owned()),
Keysym::dead_belowcircumflex => Some("".to_owned()),
Keysym::dead_belowtilde => Some("̰".to_owned()),
Keysym::dead_belowbreve => Some("̮".to_owned()),
Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
Keysym::dead_invertedbreve => Some("̯".to_owned()),
Keysym::dead_belowcomma => Some("̦".to_owned()),
Keysym::dead_currency => None,
Keysym::dead_lowline => None,
Keysym::dead_aboveverticalline => None,
Keysym::dead_belowverticalline => None,
Keysym::dead_longsolidusoverlay => None,
Keysym::dead_a => None,
Keysym::dead_A => None,
Keysym::dead_e => None,
Keysym::dead_E => None,
Keysym::dead_i => None,
Keysym::dead_I => None,
Keysym::dead_o => None,
Keysym::dead_O => None,
Keysym::dead_u => None,
Keysym::dead_U => None,
Keysym::dead_small_schwa => Some("ə".to_owned()),
Keysym::dead_capital_schwa => Some("Ə".to_owned()),
Keysym::dead_greek => None,
_ => None,
}
}
}
impl Modifiers {

View file

@ -1,5 +1,6 @@
use core::hash;
use std::cell::{RefCell, RefMut};
use std::ffi::OsString;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::path::PathBuf;
use std::rc::{Rc, Weak};
@ -42,6 +43,12 @@ use wayland_protocols::wp::cursor_shape::v1::client::{
use wayland_protocols::wp::fractional_scale::v1::client::{
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
};
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{
ContentHint, ContentPurpose,
};
use wayland_protocols::wp::text_input::zv3::client::{
zwp_text_input_manager_v3, zwp_text_input_v3,
};
use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter};
use wayland_protocols::xdg::activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1};
use wayland_protocols::xdg::decoration::zv1::client::{
@ -53,7 +60,7 @@ use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
use super::window::{WaylandWindowState, WaylandWindowStatePtr};
use super::window::{ImeInput, WaylandWindowState, WaylandWindowStatePtr};
use crate::platform::linux::is_within_click_distance;
use crate::platform::linux::wayland::cursor::Cursor;
use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
@ -87,6 +94,7 @@ pub struct Globals {
Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
pub executor: ForegroundExecutor,
}
@ -122,6 +130,7 @@ impl Globals {
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
executor,
qh,
}
@ -135,11 +144,14 @@ pub(crate) struct WaylandClientState {
wl_pointer: Option<wl_pointer::WlPointer>,
cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
data_device: Option<wl_data_device::WlDataDevice>,
text_input: Option<zwp_text_input_v3::ZwpTextInputV3>,
pre_edit_text: Option<String>,
// Surface to Window mapping
windows: HashMap<ObjectId, WaylandWindowStatePtr>,
// Output to scale mapping
output_scales: HashMap<ObjectId, i32>,
keymap_state: Option<xkb::State>,
compose_state: Option<xkb::compose::State>,
drag: DragState,
click: ClickState,
repeat: KeyRepeat,
@ -241,6 +253,9 @@ impl Drop for WaylandClient {
if let Some(data_device) = &state.data_device {
data_device.release();
}
if let Some(text_input) = &state.text_input {
text_input.destroy();
}
}
}
@ -334,10 +349,13 @@ impl WaylandClient {
wl_pointer: None,
cursor_shape_device: None,
data_device,
text_input: None,
pre_edit_text: None,
output_scales: outputs,
windows: HashMap::default(),
common,
keymap_state: None,
compose_state: None,
drag: DragState {
data_offer: None,
window: None,
@ -577,6 +595,7 @@ delegate_noop!(WaylandClientStatePtr: ignore wl_region::WlRegion);
delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1);
delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager);
delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3);
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur);
delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter);
delegate_noop!(WaylandClientStatePtr: ignore wp_viewport::WpViewport);
@ -753,12 +772,17 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
capabilities: WEnum::Value(capabilities),
} = event
{
let client = state.get_client();
let mut state = client.borrow_mut();
if capabilities.contains(wl_seat::Capability::Keyboard) {
seat.get_keyboard(qh, ());
state.text_input = state
.globals
.text_input_manager
.as_ref()
.map(|text_input_manager| text_input_manager.get_text_input(&seat, qh, ()));
}
if capabilities.contains(wl_seat::Capability::Pointer) {
let client = state.get_client();
let mut state = client.borrow_mut();
let pointer = seat.get_pointer(qh, ());
state.cursor_shape_device = state
.globals
@ -798,9 +822,10 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
wl_keyboard::KeymapFormat::XkbV1,
"Unsupported keymap format"
);
let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
let keymap = unsafe {
xkb::Keymap::new_from_fd(
&xkb::Context::new(xkb::CONTEXT_NO_FLAGS),
&xkb_context,
fd,
size as usize,
XKB_KEYMAP_FORMAT_TEXT_V1,
@ -810,7 +835,21 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
.flatten()
.expect("Failed to create keymap")
};
let table = {
let locale = std::env::var_os("LC_CTYPE").unwrap_or(OsString::from("C"));
xkb::compose::Table::new_from_locale(
&xkb_context,
&locale,
xkb::compose::COMPILE_NO_FLAGS,
)
.log_err()
.unwrap()
};
state.keymap_state = Some(xkb::State::new(&keymap));
state.compose_state = Some(xkb::compose::State::new(
&table,
xkb::compose::STATE_NO_FLAGS,
));
}
wl_keyboard::Event::Enter { surface, .. } => {
state.keyboard_focused_window = get_window(&mut state, &surface.id());
@ -827,7 +866,12 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
state.enter_token.take();
if let Some(window) = keyboard_focused_window {
if let Some(ref mut compose) = state.compose_state {
compose.reset();
}
state.pre_edit_text.take();
drop(state);
window.handle_ime(ImeInput::DeleteText);
window.set_focused(false);
}
}
@ -874,8 +918,47 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
match key_state {
wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => {
let mut keystroke =
Keystroke::from_xkb(&keymap_state, state.modifiers, keycode);
if let Some(mut compose) = state.compose_state.take() {
compose.feed(keysym);
match compose.status() {
xkb::Status::Composing => {
state.pre_edit_text =
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
let pre_edit =
state.pre_edit_text.clone().unwrap_or(String::default());
drop(state);
focused_window.handle_ime(ImeInput::SetMarkedText(pre_edit));
state = client.borrow_mut();
}
xkb::Status::Composed => {
state.pre_edit_text.take();
keystroke.ime_key = compose.utf8();
keystroke.key = xkb::keysym_get_name(compose.keysym().unwrap());
}
xkb::Status::Cancelled => {
let pre_edit = state.pre_edit_text.take();
drop(state);
if let Some(pre_edit) = pre_edit {
focused_window.handle_ime(ImeInput::InsertText(pre_edit));
}
if let Some(current_key) =
Keystroke::underlying_dead_key(keysym)
{
focused_window
.handle_ime(ImeInput::SetMarkedText(current_key));
}
compose.feed(keysym);
state = client.borrow_mut();
}
_ => {}
}
state.compose_state = Some(compose);
}
let input = PlatformInput::KeyDown(KeyDownEvent {
keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode),
keystroke: keystroke,
is_held: false, // todo(linux)
});
@ -932,6 +1015,86 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
}
}
}
impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
fn event(
this: &mut Self,
text_input: &zwp_text_input_v3::ZwpTextInputV3,
event: <zwp_text_input_v3::ZwpTextInputV3 as Proxy>::Event,
data: &(),
conn: &Connection,
qhandle: &QueueHandle<Self>,
) {
let client = this.get_client();
let mut state = client.borrow_mut();
match event {
zwp_text_input_v3::Event::Enter { surface } => {
text_input.enable();
text_input.set_content_type(ContentHint::None, ContentPurpose::Normal);
if let Some(window) = state.keyboard_focused_window.clone() {
drop(state);
if let Some(area) = window.get_ime_area() {
text_input.set_cursor_rectangle(
area.origin.x.0 as i32,
area.origin.y.0 as i32,
area.size.width.0 as i32,
area.size.height.0 as i32,
);
}
}
text_input.commit();
}
zwp_text_input_v3::Event::Leave { surface } => {
text_input.disable();
text_input.commit();
}
zwp_text_input_v3::Event::CommitString { text } => {
let Some(window) = state.keyboard_focused_window.clone() else {
return;
};
if let Some(commit_text) = text {
drop(state);
window.handle_ime(ImeInput::InsertText(commit_text));
}
}
zwp_text_input_v3::Event::PreeditString {
text,
cursor_begin,
cursor_end,
} => {
state.pre_edit_text = text;
}
zwp_text_input_v3::Event::Done { serial } => {
let last_serial = state.serial_tracker.get(SerialKind::InputMethod);
state.serial_tracker.update(SerialKind::InputMethod, serial);
let Some(window) = state.keyboard_focused_window.clone() else {
return;
};
if let Some(text) = state.pre_edit_text.take() {
drop(state);
window.handle_ime(ImeInput::SetMarkedText(text));
if let Some(area) = window.get_ime_area() {
text_input.set_cursor_rectangle(
area.origin.x.0 as i32,
area.origin.y.0 as i32,
area.size.width.0 as i32,
area.size.height.0 as i32,
);
if last_serial == serial {
text_input.commit();
}
}
} else {
drop(state);
window.handle_ime(ImeInput::DeleteText);
}
}
_ => {}
}
}
}
fn linux_button_to_gpui(button: u32) -> Option<MouseButton> {
// These values are coming from <linux/input-event-codes.h>.
@ -1053,6 +1216,16 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
}
match button_state {
wl_pointer::ButtonState::Pressed => {
if let (Some(window), Some(text), Some(compose_state)) = (
state.keyboard_focused_window.clone(),
state.pre_edit_text.take(),
state.compose_state.as_mut(),
) {
compose_state.reset();
drop(state);
window.handle_ime(ImeInput::InsertText(text));
state = client.borrow_mut();
}
let click_elapsed = state.click.last_click.elapsed();
if click_elapsed < DOUBLE_CLICK_INTERVAL

View file

@ -5,6 +5,7 @@ use collections::HashMap;
#[derive(Debug, Hash, PartialEq, Eq)]
pub(crate) enum SerialKind {
DataDevice,
InputMethod,
MouseEnter,
MousePress,
KeyPress,

View file

@ -2,6 +2,7 @@ use std::any::Any;
use std::cell::{Ref, RefCell, RefMut};
use std::ffi::c_void;
use std::num::NonZeroU32;
use std::ops::Range;
use std::ptr::NonNull;
use std::rc::{Rc, Weak};
use std::sync::Arc;
@ -162,6 +163,11 @@ impl WaylandWindowState {
}
pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr);
pub enum ImeInput {
InsertText(String),
SetMarkedText(String),
DeleteText,
}
impl Drop for WaylandWindow {
fn drop(&mut self) {
@ -425,6 +431,40 @@ impl WaylandWindowStatePtr {
}
}
pub fn handle_ime(&self, ime: ImeInput) {
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
match ime {
ImeInput::InsertText(text) => {
input_handler.replace_text_in_range(None, &text);
}
ImeInput::SetMarkedText(text) => {
input_handler.replace_and_mark_text_in_range(None, &text, None);
}
ImeInput::DeleteText => {
if let Some(marked) = input_handler.marked_text_range() {
input_handler.replace_text_in_range(Some(marked), "");
}
}
}
self.state.borrow_mut().input_handler = Some(input_handler);
}
}
pub fn get_ime_area(&self) -> Option<Bounds<Pixels>> {
let mut state = self.state.borrow_mut();
let mut bounds: Option<Bounds<Pixels>> = None;
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
if let Some(range) = input_handler.selected_text_range() {
bounds = input_handler.bounds_for_range(range);
}
self.state.borrow_mut().input_handler = Some(input_handler);
}
bounds
}
pub fn set_size_and_scale(
&self,
width: Option<NonZeroU32>,