Skip to main content

smithay_client_toolkit/seat/
input_method.rs

1//! Implementation of the `input-method-unstable-v2` protocol.
2//!
3//! This protocol allows applications to act as input methods for compositors.
4//!
5//! ### Implementation status
6//! Currently only the input-method object is supported. No keyboard grab, no popup surface.
7
8use crate::globals::GlobalData;
9
10use log::warn;
11
12use std::num::Wrapping;
13use std::sync::Mutex;
14
15use wayland_client::globals::{BindError, GlobalList};
16use wayland_client::protocol::wl_seat::WlSeat;
17use wayland_client::WEnum;
18
19use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
20use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{
21    ChangeCause, ContentHint, ContentPurpose,
22};
23pub use wayland_protocols_misc::zwp_input_method_v2::client::zwp_input_method_v2::ZwpInputMethodV2;
24use wayland_protocols_misc::zwp_input_method_v2::client::{
25    zwp_input_method_manager_v2::{self, ZwpInputMethodManagerV2},
26    zwp_input_method_v2,
27};
28
29use crate::dispatch2::Dispatch2;
30
31#[derive(Debug)]
32pub struct InputMethodManager {
33    manager: ZwpInputMethodManagerV2,
34}
35
36impl InputMethodManager {
37    /// Bind `zwp_input_method_v2` global, if it exists
38    pub fn bind<D>(globals: &GlobalList, qh: &QueueHandle<D>) -> Result<Self, BindError>
39    where
40        D: Dispatch<ZwpInputMethodManagerV2, GlobalData> + 'static,
41    {
42        let manager = globals.bind(qh, 1..=1, GlobalData)?;
43        Ok(Self { manager })
44    }
45
46    /// Request a new input zwp_input_method_v2 object associated with a given
47    /// seat.
48    pub fn get_input_method<State>(&self, qh: &QueueHandle<State>, seat: &WlSeat) -> InputMethod
49    where
50        State: Dispatch<ZwpInputMethodV2, InputMethodData<()>, State> + 'static,
51    {
52        self.get_input_method_with_data(qh, seat, ())
53    }
54
55    pub fn get_input_method_with_data<State, U>(
56        &self,
57        qh: &QueueHandle<State>,
58        seat: &WlSeat,
59        udata: U,
60    ) -> InputMethod
61    where
62        State: Dispatch<ZwpInputMethodV2, InputMethodData<U>, State> + 'static,
63        U: Send + Sync + 'static,
64    {
65        InputMethod {
66            input_method: self.manager.get_input_method(
67                seat,
68                qh,
69                InputMethodData::new(seat.clone(), udata),
70            ),
71        }
72    }
73}
74
75impl<D> Dispatch2<zwp_input_method_manager_v2::ZwpInputMethodManagerV2, D> for GlobalData
76where
77    D: InputMethodHandler,
78{
79    fn event(
80        &self,
81        _data: &mut D,
82        _manager: &zwp_input_method_manager_v2::ZwpInputMethodManagerV2,
83        _event: zwp_input_method_manager_v2::Event,
84        _conn: &Connection,
85        _qh: &QueueHandle<D>,
86    ) {
87        unreachable!()
88    }
89}
90
91#[derive(Debug)]
92pub struct InputMethod {
93    input_method: ZwpInputMethodV2,
94}
95
96impl InputMethod {
97    pub fn set_preedit_string(&self, text: String, cursor: CursorPosition) {
98        // TODO: should this enforce indices on codepoint boundaries?
99        let (start, end) = match cursor {
100            CursorPosition::Hidden => (-1, -1),
101            CursorPosition::Visible { start, end } => (
102                // This happens only for cursor values in the upper usize range.
103                // Such values are most likely bugs already,
104                // so it's not a problem if one of the cursors weirdly lands at 0 sometimes.
105                start.try_into().unwrap_or(0),
106                end.try_into().unwrap_or(0),
107            ),
108        };
109        self.input_method.set_preedit_string(text, start, end)
110    }
111
112    pub fn commit_string(&self, text: String) {
113        self.input_method.commit_string(text)
114    }
115
116    pub fn delete_surrounding_text(&self, before_length: u32, after_length: u32) {
117        // TODO: this has 2 separate behaviours:
118        // one when preedit text is supported,
119        // and a completely different one when it is not supported
120        // and the input method doesn't know what bytes it deletes.
121        // Not sure how or whether this should be reflected here.
122        self.input_method.delete_surrounding_text(before_length, after_length)
123    }
124
125    pub fn commit<U: Send + Sync + 'static>(&self) {
126        let data = self.input_method.data::<InputMethodData<U>>().unwrap();
127        let inner = data.inner.lock().unwrap();
128        self.input_method.commit(inner.serial.0)
129    }
130}
131
132#[derive(Debug)]
133pub struct InputMethodData<U> {
134    seat: WlSeat,
135    inner: Mutex<InputMethodDataInner>,
136    udata: U,
137}
138
139impl<U> InputMethodData<U> {
140    /// Create the new input method data associated with the given seat.
141    pub fn new(seat: WlSeat, udata: U) -> Self {
142        Self {
143            seat,
144            inner: Mutex::new(InputMethodDataInner {
145                pending_state: Default::default(),
146                current_state: Default::default(),
147                serial: Wrapping(0),
148            }),
149            udata,
150        }
151    }
152
153    pub fn data(&self) -> &U {
154        &self.udata
155    }
156
157    pub fn data_mut(&mut self) -> &mut U {
158        &mut self.udata
159    }
160
161    /// Get the associated seat from the data.
162    pub fn seat(&self) -> &WlSeat {
163        &self.seat
164    }
165}
166
167#[derive(Debug)]
168struct InputMethodDataInner {
169    pending_state: InputMethodEventState,
170    current_state: InputMethodEventState,
171    serial: Wrapping<u32>,
172}
173
174/// Stores incoming interface state.
175#[derive(Debug, Clone, PartialEq)]
176pub struct InputMethodEventState {
177    pub surrounding: SurroundingText,
178    pub content_purpose: ContentPurpose,
179    pub content_hint: ContentHint,
180    pub text_change_cause: ChangeCause,
181    pub active: Active,
182}
183
184impl Default for InputMethodEventState {
185    fn default() -> Self {
186        Self {
187            surrounding: SurroundingText::default(),
188            content_hint: ContentHint::empty(),
189            content_purpose: ContentPurpose::Normal,
190            text_change_cause: ChangeCause::InputMethod,
191            active: Active::default(),
192        }
193    }
194}
195
196#[derive(Clone, Copy, Debug, PartialEq)]
197pub enum CursorPosition {
198    Hidden,
199    Visible { start: usize, end: usize },
200}
201
202#[derive(Default, Clone, Debug, PartialEq)]
203pub struct SurroundingText {
204    pub text: String,
205    pub cursor: u32,
206    pub anchor: u32,
207}
208
209/// State machine for determining the capabilities of a text input
210#[derive(Clone, Debug, Default, Copy, PartialEq)]
211pub enum Active {
212    #[default]
213    Inactive,
214    NegotiatingCapabilities {
215        surrounding_text: bool,
216        content_type: bool,
217    },
218    Active {
219        surrounding_text: bool,
220        content_type: bool,
221    },
222}
223
224impl Active {
225    fn with_active(self) -> Self {
226        match self {
227            Self::Inactive => {
228                Self::NegotiatingCapabilities { content_type: false, surrounding_text: false }
229            }
230            other => other,
231        }
232    }
233
234    fn with_surrounding_text(self) -> Self {
235        match self {
236            Self::Inactive => Self::Inactive,
237            Self::NegotiatingCapabilities { content_type, .. } => {
238                Self::NegotiatingCapabilities { content_type, surrounding_text: true }
239            }
240            active @ Self::Active { .. } => active,
241        }
242    }
243
244    fn with_content_type(self) -> Self {
245        match self {
246            Self::Inactive => Self::Inactive,
247            Self::NegotiatingCapabilities { surrounding_text, .. } => {
248                Self::NegotiatingCapabilities { content_type: true, surrounding_text }
249            }
250            active @ Self::Active { .. } => active,
251        }
252    }
253
254    fn with_done(self) -> Self {
255        match self {
256            Self::Inactive => Self::Inactive,
257            Self::NegotiatingCapabilities { surrounding_text, content_type } => {
258                Self::Active { content_type, surrounding_text }
259            }
260            active @ Self::Active { .. } => active,
261        }
262    }
263}
264
265pub trait InputMethodHandler: Sized {
266    fn handle_done(
267        &self,
268        connection: &Connection,
269        qh: &QueueHandle<Self>,
270        input_method: &ZwpInputMethodV2,
271        state: &InputMethodEventState,
272    );
273    fn handle_unavailable(
274        &self,
275        connection: &Connection,
276        qh: &QueueHandle<Self>,
277        input_method: &ZwpInputMethodV2,
278    );
279}
280
281impl<D, U> Dispatch2<ZwpInputMethodV2, D> for InputMethodData<U>
282where
283    D: InputMethodHandler,
284{
285    fn event(
286        &self,
287        data: &mut D,
288        input_method: &ZwpInputMethodV2,
289        event: zwp_input_method_v2::Event,
290        conn: &Connection,
291        qh: &QueueHandle<D>,
292    ) {
293        let mut imdata: std::sync::MutexGuard<'_, InputMethodDataInner> =
294            self.inner.lock().unwrap();
295
296        use zwp_input_method_v2::Event;
297
298        match event {
299            Event::Activate => {
300                imdata.pending_state = InputMethodEventState {
301                    active: imdata.pending_state.active.with_active(),
302                    ..Default::default()
303                };
304            }
305            Event::Deactivate => {
306                imdata.pending_state = Default::default();
307            }
308            Event::SurroundingText { text, cursor, anchor } => {
309                imdata.pending_state = InputMethodEventState {
310                    active: imdata.pending_state.active.with_surrounding_text(),
311                    surrounding: SurroundingText { text, cursor, anchor },
312                    ..imdata.pending_state.clone()
313                }
314            }
315            Event::TextChangeCause { cause } => {
316                imdata.pending_state = InputMethodEventState {
317                    text_change_cause: match cause {
318                        WEnum::Value(cause) => cause,
319                        WEnum::Unknown(value) => {
320                            warn!(
321                                "Unknown `text_change_cause`: {}. Assuming not input method.",
322                                value
323                            );
324                            ChangeCause::Other
325                        }
326                    },
327                    ..imdata.pending_state.clone()
328                }
329            }
330            Event::ContentType { hint, purpose } => {
331                imdata.pending_state = InputMethodEventState {
332                    active: imdata.pending_state.active.with_content_type(),
333                    content_hint: match hint {
334                        WEnum::Value(hint) => hint,
335                        WEnum::Unknown(value) => {
336                            warn!(
337                                "Unknown content hints: 0b{:b}, ignoring.",
338                                ContentHint::from_bits_retain(value)
339                                    - ContentHint::from_bits_truncate(value)
340                            );
341                            ContentHint::from_bits_truncate(value)
342                        }
343                    },
344                    content_purpose: match purpose {
345                        WEnum::Value(v) => v,
346                        WEnum::Unknown(value) => {
347                            warn!("Unknown `content_purpose`: {}. Assuming `normal`.", value);
348                            ContentPurpose::Normal
349                        }
350                    },
351                    ..imdata.pending_state.clone()
352                }
353            }
354            Event::Done => {
355                imdata.pending_state = InputMethodEventState {
356                    active: imdata.pending_state.active.with_done(),
357                    ..imdata.pending_state.clone()
358                };
359                imdata.current_state = imdata.pending_state.clone();
360                imdata.serial += 1;
361                data.handle_done(conn, qh, input_method, &imdata.current_state)
362            }
363            Event::Unavailable => data.handle_unavailable(conn, qh, input_method),
364            _ => unreachable!(),
365        };
366    }
367}
368
369#[cfg(test)]
370mod test {
371    use super::*;
372
373    struct Handler {}
374
375    impl InputMethodHandler for Handler {
376        fn handle_done(
377            &self,
378            _conn: &Connection,
379            _qh: &QueueHandle<Self>,
380            _input_method: &ZwpInputMethodV2,
381            _state: &InputMethodEventState,
382        ) {
383        }
384
385        fn handle_unavailable(
386            &self,
387            _conn: &Connection,
388            _qh: &QueueHandle<Self>,
389            _input_method: &ZwpInputMethodV2,
390        ) {
391        }
392    }
393
394    crate::delegate_dispatch2!(Handler);
395
396    fn assert_is_manager_delegate<T>()
397    where
398        T: wayland_client::Dispatch<ZwpInputMethodManagerV2, crate::globals::GlobalData>,
399    {
400    }
401
402    fn assert_is_delegate<T>()
403    where
404        T: wayland_client::Dispatch<ZwpInputMethodV2, InputMethodData<()>>,
405    {
406    }
407
408    #[test]
409    fn test_valid_assignment() {
410        assert_is_manager_delegate::<Handler>();
411        assert_is_delegate::<Handler>();
412    }
413}