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:
parent
fdadbc7174
commit
5596a34311
4 changed files with 281 additions and 5 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
{
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) {
|
||||
seat.get_keyboard(qh, ());
|
||||
}
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) {
|
||||
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 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
|
||||
|
|
|
@ -5,6 +5,7 @@ use collections::HashMap;
|
|||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub(crate) enum SerialKind {
|
||||
DataDevice,
|
||||
InputMethod,
|
||||
MouseEnter,
|
||||
MousePress,
|
||||
KeyPress,
|
||||
|
|
|
@ -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>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue