Skip to main content

smithay_client_toolkit/seat/
input_method_v3.rs

1/*! This implements support for the experimental xx-input-method-v2 protocol.
2 * That protocol will hopefully become -v3 without changing the API at some point.
3 *
4 *
5 * This is a low-level interface to the input method. It will generally not check if the client is allowed to issue a request in context, e.g. when the input method is inactive.
6 *
7 * It does handle some serials for the client, as well as it checks the validity of values for the current protocol version.
8 *
9 * The client is responsible for avoiding protocol errors.
10 */
11
12use crate::compositor::Surface;
13use crate::globals::GlobalData;
14
15use log::{debug, warn};
16
17use std::collections::{HashMap, HashSet};
18use std::num::Wrapping;
19use std::ops::Deref;
20use std::sync::{Arc, Mutex, MutexGuard, Weak};
21
22use crate::reexports::protocols_experimental::text_input::v3::client::xx_text_input_v3::{
23    Action, ChangeCause, ContentHint, ContentPurpose, SupportedFeatures,
24};
25use wayland_client::globals::{BindError, GlobalList};
26use wayland_client::protocol::wl_seat::WlSeat;
27use wayland_client::protocol::wl_surface;
28use wayland_client::WEnum;
29use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
30
31use crate::reexports::protocols_experimental::input_method::v1::client as protocol;
32
33use crate::dispatch2::Dispatch2;
34
35pub use protocol::xx_input_method_v1::XxInputMethodV1;
36pub use protocol::xx_input_popup_positioner_v1::XxInputPopupPositionerV1;
37pub use protocol::xx_input_popup_surface_v2::XxInputPopupSurfaceV2;
38
39use protocol::{
40    xx_input_method_manager_v2::{self, XxInputMethodManagerV2},
41    xx_input_method_v1, xx_input_popup_positioner_v1, xx_input_popup_surface_v2,
42};
43
44pub use xx_input_popup_positioner_v1::{Anchor, Gravity};
45
46#[derive(Debug, PartialEq, Eq, Clone)]
47pub struct Size {
48    pub width: u32,
49    pub height: u32,
50}
51
52#[derive(Debug, PartialEq, Eq, Clone)]
53pub struct Rectangle {
54    pub x: i32,
55    pub y: i32,
56    pub width: u32,
57    pub height: u32,
58}
59
60#[derive(Debug)]
61pub struct InputMethodManager {
62    manager: XxInputMethodManagerV2,
63}
64
65impl InputMethodManager {
66    /// Bind the input_method global, if it exists
67    pub fn bind<D>(globals: &GlobalList, qh: &QueueHandle<D>) -> Result<Self, BindError>
68    where
69        D: Dispatch<XxInputMethodManagerV2, GlobalData> + 'static,
70    {
71        let manager = globals.bind(qh, 2..=3, GlobalData)?;
72        Ok(Self { manager })
73    }
74
75    /// Request a new input_method object associated with a given
76    /// seat.
77    pub fn get_input_method<State>(&self, qh: &QueueHandle<State>, seat: &WlSeat) -> InputMethod
78    where
79        State: Dispatch<XxInputMethodV1, InputMethodData<()>, State> + 'static,
80    {
81        self.get_input_method_with_data(qh, seat, ())
82    }
83
84    pub fn get_input_method_with_data<State, U>(
85        &self,
86        qh: &QueueHandle<State>,
87        seat: &WlSeat,
88        udata: U,
89    ) -> InputMethod
90    where
91        State: Dispatch<XxInputMethodV1, InputMethodData<U>, State> + 'static,
92        U: Send + Sync + 'static,
93    {
94        InputMethod {
95            input_method: self.manager.get_input_method(
96                seat,
97                qh,
98                InputMethodData::new(seat.clone(), udata),
99            ),
100        }
101    }
102
103    pub fn get_positioner<State>(&self, qh: &QueueHandle<State>) -> PopupPositioner
104    where
105        State: Dispatch<XxInputPopupPositionerV1, PositionerData, State> + 'static,
106    {
107        PopupPositioner(self.manager.get_positioner(qh, PositionerData))
108    }
109}
110
111impl<D> Dispatch2<xx_input_method_manager_v2::XxInputMethodManagerV2, D> for GlobalData
112where
113    D: InputMethodHandler,
114{
115    fn event(
116        &self,
117        _data: &mut D,
118        _manager: &xx_input_method_manager_v2::XxInputMethodManagerV2,
119        _event: xx_input_method_manager_v2::Event,
120        _conn: &Connection,
121        _qh: &QueueHandle<D>,
122    ) {
123        unreachable!()
124    }
125}
126
127/// A trivial wrapper for an [`XxInputPopupPositionerV1`].
128///
129/// This wrapper calls [`destroy`][XxInputPopupPositionerV1::destroy] on the contained
130/// positioner when it is dropped.
131#[derive(Debug)]
132pub struct PopupPositioner(XxInputPopupPositionerV1);
133
134impl Deref for PopupPositioner {
135    type Target = XxInputPopupPositionerV1;
136
137    fn deref(&self) -> &Self::Target {
138        &self.0
139    }
140}
141
142impl Drop for PopupPositioner {
143    fn drop(&mut self) {
144        self.0.destroy()
145    }
146}
147
148impl<D> Dispatch2<XxInputPopupPositionerV1, D> for PositionerData
149where
150    D: InputMethodHandler,
151{
152    fn event(
153        &self,
154        _data: &mut D,
155        _manager: &XxInputPopupPositionerV1,
156        _event: xx_input_popup_positioner_v1::Event,
157        _conn: &Connection,
158        _qh: &QueueHandle<D>,
159    ) {
160        unreachable!("Positioner has no events")
161    }
162}
163
164#[derive(Debug)]
165pub struct PositionerData;
166
167#[derive(Debug)]
168pub struct InputMethod {
169    input_method: XxInputMethodV1,
170}
171
172/// Can't set the preedit string due to cursor index not on UTF-8 code point boundary
173#[derive(Debug)]
174pub enum InvalidIndex {
175    /// Only the start index is not on the boundary
176    Start,
177    /// Only the end index is not on the boundary
178    End,
179    /// Both the start and end indices are not on boundaries
180    Both,
181}
182
183impl InputMethod {
184    pub fn input_method(&self) -> &XxInputMethodV1 {
185        &self.input_method
186    }
187
188    pub fn set_preedit_string(
189        &self,
190        text: String,
191        cursor: CursorPosition,
192    ) -> Result<(), InvalidIndex> {
193        let (start, end) = match cursor {
194            CursorPosition::Hidden => (-1, -1),
195            CursorPosition::Visible { start, end } => {
196                match (text.is_char_boundary(start), text.is_char_boundary(end)) {
197                    (true, true) => (
198                        // This happens only for cursor values in the upper usize range.
199                        // Such values are most likely bugs already,
200                        // so it's not a problem if one of the cursors weirdly lands at 0 sometimes.
201                        start.try_into().unwrap_or(0),
202                        end.try_into().unwrap_or(0),
203                    ),
204                    (true, false) => {
205                        return Err(InvalidIndex::End);
206                    }
207                    (false, true) => {
208                        return Err(InvalidIndex::Start);
209                    }
210                    (false, false) => {
211                        return Err(InvalidIndex::Both);
212                    }
213                }
214            }
215        };
216        self.input_method.set_preedit_string(text, start, end);
217        Ok(())
218    }
219
220    pub fn commit_string(&self, text: String) {
221        self.input_method.commit_string(text)
222    }
223
224    pub fn delete_surrounding_text(&self, before_length: u32, after_length: u32) {
225        // TODO: this has 2 separate behaviours:
226        // one when surrounding text is supported,
227        // and a completely different one when it is not supported
228        // and the input method doesn't know what bytes it deletes.
229        // Not sure how or whether this should be reflected here.
230        self.input_method.delete_surrounding_text(before_length, after_length)
231    }
232
233    /// This method doesn't check if the action has been made available for this text input.
234    pub fn perform_action(&self, action: Action) {
235        self.input_method.perform_action(action)
236    }
237
238    pub fn move_cursor(&self, cursor: i32, anchor: i32) {
239        self.input_method.move_cursor(cursor, anchor)
240    }
241
242    pub fn commit<U>(&self)
243    where
244        U: Send + Sync + 'static,
245    {
246        let data = self.input_method.data::<InputMethodData<U>>().unwrap();
247        let inner = &data.inner.lock().unwrap();
248        self.input_method.commit(inner.serial.0)
249    }
250
251    pub fn get_input_popup_surface<D, U>(
252        &self,
253        qh: &QueueHandle<D>,
254        surface: impl Into<Surface>,
255        positioner: &PopupPositioner,
256    ) -> Popup
257    where
258        D: Dispatch<XxInputPopupSurfaceV2, PopupData> + 'static,
259        U: Send + Sync + 'static,
260    {
261        let data = self.input_method.data::<InputMethodData<U>>().unwrap();
262        let surface = surface.into();
263        Popup {
264            input_method: self.input_method.clone(),
265            popup: self.input_method.get_input_popup_surface(
266                surface.wl_surface(),
267                &positioner.0,
268                qh,
269                PopupData { inner: Mutex::new(PopupDataInner::new(Arc::downgrade(&data.inner))) },
270            ),
271            surface,
272        }
273    }
274}
275
276#[derive(Debug)]
277pub struct InputMethodData<U> {
278    seat: WlSeat,
279    inner: Arc<Mutex<InputMethodDataInner>>,
280    udata: U,
281}
282
283impl<U> InputMethodData<U> {
284    /// Create the new touch data associated with the given seat.
285    pub fn new(seat: WlSeat, udata: U) -> Self {
286        Self {
287            seat,
288            inner: Arc::new(Mutex::new(InputMethodDataInner {
289                pending_state: Default::default(),
290                current_state: Default::default(),
291                serial: Wrapping(0),
292            })),
293            udata,
294        }
295    }
296
297    pub fn data(&self) -> &U {
298        &self.udata
299    }
300
301    pub fn data_mut(&mut self) -> &mut U {
302        &mut self.udata
303    }
304
305    /// Get the associated seat from the data.
306    pub fn seat(&self) -> &WlSeat {
307        &self.seat
308    }
309}
310
311#[derive(Debug)]
312struct InputMethodDataInner {
313    pending_state: InputMethodEventState,
314    current_state: InputMethodEventState,
315    serial: Wrapping<u32>,
316}
317
318/// Stores incoming interface state.
319#[derive(Debug, Clone, PartialEq)]
320pub struct InputMethodEventState {
321    pub surrounding: SurroundingText,
322    pub content_purpose: ContentPurpose,
323    pub content_hint: ContentHint,
324    pub text_change_cause: ChangeCause,
325    pub active: Active,
326    pub popups: HashMap<XxInputPopupSurfaceV2, PopupState>,
327}
328
329impl Default for InputMethodEventState {
330    fn default() -> Self {
331        Self {
332            surrounding: SurroundingText::default(),
333            content_hint: ContentHint::empty(),
334            content_purpose: ContentPurpose::Normal,
335            text_change_cause: ChangeCause::InputMethod,
336            active: Active::default(),
337            popups: Default::default(),
338        }
339    }
340}
341
342/// Server-provided popup state
343#[derive(Clone, Debug, PartialEq)]
344pub struct PopupState {
345    /// The position of the anchor relative to top-left corner of the popup
346    pub anchor: Rectangle,
347    pub size: Size,
348    /// serial == None means there is no configure sequence open and attempts to change state must be ignored.
349    pub serial: Option<u32>,
350    /// The repositioned token from the last sequence
351    pub repositioned: Option<u32>,
352}
353
354impl PopupState {
355    /// Creates an uninitialized copy ready to fill in
356    fn new_uninit() -> Self {
357        Self {
358            // The protocol doesn't allow reading size or anchor before writing, so the values don't matter
359            anchor: Rectangle { x: 0, y: 0, width: 0, height: 0 },
360            size: Size { width: 0, height: 0 },
361            serial: None,
362            repositioned: None,
363        }
364    }
365
366    /// Returns a copy after resetting the fields as required by the protocol on input_method.done.
367    fn reset_on_done(&self) -> Self {
368        Self { serial: None, repositioned: None, ..self.clone() }
369    }
370}
371
372#[derive(Clone, Copy, Debug, PartialEq)]
373pub enum CursorPosition {
374    Hidden,
375    // Bytes relative to the beginning of the text. Must fall on code point boundaries.
376    Visible { start: usize, end: usize },
377}
378
379#[derive(Default, Clone, Debug, PartialEq)]
380pub struct SurroundingText {
381    pub text: String,
382    pub cursor: u32,
383    pub anchor: u32,
384}
385
386/// Describes operations that can be performed on this input method.
387#[non_exhaustive]
388// non exhaustive so that bumping protocol version and adding new ones
389// doesn't automatically break compat
390#[derive(Clone, Debug, PartialEq)]
391pub struct Capabilities {
392    pub surrounding_text: bool,
393    pub content_type: bool,
394    pub actions: HashSet<Action>,
395    pub supported_features: SupportedFeatures,
396}
397
398impl Default for Capabilities {
399    fn default() -> Self {
400        Self {
401            surrounding_text: false,
402            content_type: false,
403            actions: Default::default(),
404            supported_features: SupportedFeatures::empty(),
405        }
406    }
407}
408
409/// State machine for determining the capabilities of a text input
410#[derive(Clone, Debug, Default, PartialEq)]
411pub enum Active {
412    #[default]
413    Inactive,
414    NegotiatingCapabilities(Capabilities),
415    Active(Capabilities),
416}
417
418impl Active {
419    fn with_active(self) -> Self {
420        match self {
421            Self::Inactive => Self::NegotiatingCapabilities(Capabilities::default()),
422            other => other,
423        }
424    }
425
426    fn with_surrounding_text(self) -> Self {
427        match self {
428            Self::Inactive => Self::Inactive,
429            Self::NegotiatingCapabilities(capabilities) => {
430                Self::NegotiatingCapabilities(Capabilities {
431                    surrounding_text: true,
432                    ..capabilities
433                })
434            }
435            active @ Self::Active { .. } => active,
436        }
437    }
438
439    fn with_content_type(self) -> Self {
440        match self {
441            Self::Inactive => Self::Inactive,
442            Self::NegotiatingCapabilities(capabilities) => {
443                Self::NegotiatingCapabilities(Capabilities { content_type: true, ..capabilities })
444            }
445            active @ Self::Active { .. } => active,
446        }
447    }
448
449    fn with_actions(self, actions: HashSet<Action>) -> Self {
450        match self {
451            Self::Inactive => Self::Inactive,
452            Self::NegotiatingCapabilities(capabilities) => {
453                Self::NegotiatingCapabilities(Capabilities { actions, ..capabilities })
454            }
455            active @ Self::Active { .. } => active,
456        }
457    }
458
459    fn with_extra_features(self, supported_features: SupportedFeatures) -> Self {
460        match self {
461            Self::Inactive => Self::Inactive,
462            Self::NegotiatingCapabilities(capabilities) => {
463                Self::NegotiatingCapabilities(Capabilities { supported_features, ..capabilities })
464            }
465            active @ Self::Active { .. } => active,
466        }
467    }
468
469    fn with_done(self) -> Self {
470        match self {
471            Self::Inactive => Self::Inactive,
472            Self::NegotiatingCapabilities(capabilities) => Self::Active(capabilities),
473            active @ Self::Active { .. } => active,
474        }
475    }
476}
477
478#[derive(Debug)]
479pub struct Popup {
480    /// A weak reference to the input method to which this applies
481    input_method: XxInputMethodV1,
482    popup: XxInputPopupSurfaceV2,
483    surface: Surface,
484}
485
486impl Popup {
487    pub fn wl_surface(&self) -> &wl_surface::WlSurface {
488        self.surface.wl_surface()
489    }
490
491    pub fn input_method(&self) -> &XxInputMethodV1 {
492        &self.input_method
493    }
494
495    pub fn popup(&self) -> &XxInputPopupSurfaceV2 {
496        &self.popup
497    }
498
499    pub fn reposition(&self, positioner: &PopupPositioner) {
500        let data = self.popup.data::<PopupData>().unwrap();
501        let mut inner: MutexGuard<'_, PopupDataInner> = data.inner.lock().unwrap();
502        let token = inner.next_token;
503        inner.next_token = inner.next_token.wrapping_add(1);
504        inner.outstanding_reposition_token = Some(token);
505        self.popup.reposition(positioner, token);
506    }
507}
508
509impl<D> Dispatch2<XxInputPopupSurfaceV2, D> for PopupData
510where
511    D: InputMethodHandler,
512{
513    fn event(
514        &self,
515        _data: &mut D,
516        popup: &XxInputPopupSurfaceV2,
517        event: xx_input_popup_surface_v2::Event,
518        _conn: &Connection,
519        _qh: &QueueHandle<D>,
520    ) {
521        let inner: MutexGuard<'_, PopupDataInner> = self.inner.lock().unwrap();
522        if let Some(im) = inner.im.upgrade() {
523            let mut im = im.lock().unwrap();
524
525            use xx_input_popup_surface_v2::Event;
526            match event {
527                Event::Repositioned { token } => {
528                    let state = im
529                        .pending_state
530                        .popups
531                        .entry(popup.clone())
532                        .or_insert(PopupState::new_uninit());
533                    if state.serial.is_some() {
534                        state.repositioned = Some(token);
535                    } else {
536                        warn!(
537                            "Repositioned received after im.done but before popup.start_configure"
538                        );
539                    }
540                }
541                Event::StartConfigure {
542                    width,
543                    height,
544                    anchor_x,
545                    anchor_y,
546                    anchor_width,
547                    anchor_height,
548                    serial,
549                } => {
550                    let uninit = PopupState::new_uninit();
551                    let prev_state = im.pending_state.popups.get(popup).unwrap_or(&uninit);
552                    let anchor = Rectangle {
553                        x: anchor_x,
554                        y: anchor_y,
555                        width: anchor_width,
556                        height: anchor_height,
557                    };
558                    let popup_state = PopupState {
559                        anchor,
560                        serial: Some(serial),
561                        size: Size { width, height },
562                        ..prev_state.clone()
563                    };
564                    im.pending_state.popups.insert(popup.clone(), popup_state);
565                }
566                _ => unreachable!(),
567            };
568        } else {
569            warn!("received event for an input method that already disappeared");
570        }
571    }
572}
573
574/// Data reachable from XxInputPopupSurfaceV2
575#[derive(Debug)]
576pub struct PopupData {
577    // For mutability. Data is immutable.
578    inner: Mutex<PopupDataInner>,
579}
580
581/// Mutable data reachable from XxInputPopupSurfaceV2
582#[derive(Debug)]
583struct PopupDataInner {
584    im: Weak<Mutex<InputMethodDataInner>>,
585    next_token: u32,
586    outstanding_reposition_token: Option<u32>,
587}
588
589impl PopupDataInner {
590    /// Creates a new, uninitialized state
591    fn new(im: Weak<Mutex<InputMethodDataInner>>) -> Self {
592        Self { im, next_token: 0, outstanding_reposition_token: None }
593    }
594
595    /// Returns the newly received token if it's plausibly valid.
596    fn update_repositioned(&mut self, state: &PopupState) -> Option<u32> {
597        match (state.repositioned, self.outstanding_reposition_token) {
598            (Some(_), None) => {
599                warn!("Received a repositioned token even though all were already processed. Did one arrive out of order?");
600                None
601            }
602            (None, _) => None,
603            (received, Some(outstanding)) => {
604                if received == Some(outstanding) {
605                    self.outstanding_reposition_token = None
606                } else {
607                    debug!(
608                        "Received a reposition token that is not the most recently requested one."
609                    )
610                };
611                received
612            }
613        }
614    }
615}
616
617pub trait InputMethodHandler: Sized {
618    fn handle_done(
619        &mut self,
620        qh: &QueueHandle<Self>,
621        input_method: &XxInputMethodV1,
622        state: &InputMethodEventState,
623    );
624    /*fn handle_popup_configure(
625        &self,
626        connection: &Connection,
627        qh: &QueueHandle<Self>,
628        input_method: &XxInputPopupSurfaceV2,
629        state: PopupConfigure,
630    );*/
631    fn handle_unavailable(&mut self, qh: &QueueHandle<Self>, input_method: &XxInputMethodV1);
632}
633
634impl<D, U> Dispatch2<XxInputMethodV1, D> for InputMethodData<U>
635where
636    D: InputMethodHandler,
637{
638    fn event(
639        &self,
640        data: &mut D,
641        input_method: &XxInputMethodV1,
642        event: xx_input_method_v1::Event,
643        _conn: &Connection,
644        qh: &QueueHandle<D>,
645    ) {
646        let mut imdata: MutexGuard<'_, InputMethodDataInner> = self.inner.lock().unwrap();
647
648        use xx_input_method_v1::Event;
649
650        match event {
651            Event::Activate => {
652                imdata.pending_state = InputMethodEventState {
653                    active: imdata.pending_state.active.clone().with_active(),
654                    ..Default::default()
655                };
656            }
657            Event::Deactivate => {
658                imdata.pending_state = Default::default();
659            }
660            Event::SurroundingText { text, cursor, anchor } => {
661                imdata.pending_state = InputMethodEventState {
662                    active: imdata.pending_state.active.clone().with_surrounding_text(),
663                    surrounding: SurroundingText { text, cursor, anchor },
664                    ..imdata.pending_state.clone()
665                }
666            }
667            Event::TextChangeCause { cause } => {
668                imdata.pending_state = InputMethodEventState {
669                    text_change_cause: match cause {
670                        WEnum::Value(cause) => cause,
671                        WEnum::Unknown(value) => {
672                            warn!(
673                                "Unknown `text_change_cause`: {}. Assuming not input method.",
674                                value
675                            );
676                            ChangeCause::Other
677                        }
678                    },
679                    ..imdata.pending_state.clone()
680                }
681            }
682            Event::ContentType { hint, purpose } => {
683                imdata.pending_state = InputMethodEventState {
684                    active: imdata.pending_state.active.clone().with_content_type(),
685                    content_hint: match hint {
686                        WEnum::Value(hint) => hint,
687                        WEnum::Unknown(value) => {
688                            warn!(
689                                "Unknown content hints: 0b{:b}, ignoring.",
690                                ContentHint::from_bits_retain(value)
691                                    - ContentHint::from_bits_truncate(value)
692                            );
693                            ContentHint::from_bits_truncate(value)
694                        }
695                    },
696                    content_purpose: match purpose {
697                        WEnum::Value(v) => v,
698                        WEnum::Unknown(value) => {
699                            warn!("Unknown `content_purpose`: {}. Assuming `normal`.", value);
700                            ContentPurpose::Normal
701                        }
702                    },
703                    ..imdata.pending_state.clone()
704                }
705            }
706            Event::SetAvailableActions { available_actions } => {
707                imdata.pending_state = InputMethodEventState {
708                    active: imdata.pending_state.active.clone().with_actions(
709                        HashSet::from_iter(available_actions.iter().filter_map(|num| {
710                            Action::try_from(*num as u32)
711                                .map_err(|()| warn!("Unknown available action {num}, ignoring"))
712                                .ok()
713                        }))
714                    ),
715                    ..imdata.pending_state.clone()
716                }
717            }
718            Event::AnnounceSupportedFeatures { features } => {
719                imdata.pending_state = InputMethodEventState {
720                    active: imdata.pending_state.active.clone().with_extra_features(
721                        match features {
722                            WEnum::Value(v) => v,
723                            WEnum::Unknown(value) => {
724                                warn!("Unknown `features`: {value}. Assuming no extra features supported.");
725                                SupportedFeatures::empty()
726                            }
727                        }
728                    ),
729                    ..imdata.pending_state.clone()
730                }
731            }
732            Event::Done => {
733                imdata.pending_state = InputMethodEventState {
734                    active: imdata.pending_state.active.clone().with_done(),
735                    ..imdata.pending_state.clone()
736                };
737                for (popup, state) in imdata.pending_state.popups.iter_mut() {
738                    if let Some(serial) = state.serial {
739                        popup.ack_configure(serial);
740                    }
741                    let data = popup.data::<PopupData>().unwrap();
742                    {
743                        let mut inner: MutexGuard<'_, PopupDataInner> = data.inner.lock().unwrap();
744                        inner.update_repositioned(state);
745                    }
746                    *state = state.clone().reset_on_done();
747                }
748                imdata.current_state = imdata.pending_state.clone();
749                imdata.serial += 1;
750                data.handle_done(qh, input_method, &imdata.current_state)
751            }
752            Event::Unavailable => data.handle_unavailable(qh, input_method),
753            _ => unreachable!(),
754        };
755    }
756}
757
758#[cfg(test)]
759mod test {
760    use super::*;
761
762    struct Handler {}
763
764    impl InputMethodHandler for Handler {
765        fn handle_done(
766            &mut self,
767            _qh: &QueueHandle<Self>,
768            _input_method: &XxInputMethodV1,
769            _state: &InputMethodEventState,
770        ) {
771        }
772
773        fn handle_unavailable(&mut self, _qh: &QueueHandle<Self>, _input_method: &XxInputMethodV1) {
774        }
775    }
776
777    crate::delegate_dispatch2!(Handler);
778
779    fn assert_is_manager_delegate<T>()
780    where
781        T: wayland_client::Dispatch<
782            protocol::xx_input_method_manager_v2::XxInputMethodManagerV2,
783            crate::globals::GlobalData,
784        >,
785    {
786    }
787
788    fn assert_is_delegate<T>()
789    where
790        T: wayland_client::Dispatch<
791            protocol::xx_input_method_v1::XxInputMethodV1,
792            InputMethodData<()>,
793        >,
794    {
795    }
796
797    fn assert_is_popup_delegate<T>()
798    where
799        T: wayland_client::Dispatch<
800            protocol::xx_input_popup_surface_v2::XxInputPopupSurfaceV2,
801            PopupData,
802        >,
803    {
804    }
805
806    fn assert_is_positioner_delegate<T>()
807    where
808        T: wayland_client::Dispatch<
809            protocol::xx_input_popup_positioner_v1::XxInputPopupPositionerV1,
810            PositionerData,
811        >,
812    {
813    }
814
815    #[test]
816    fn test_valid_assignment() {
817        assert_is_manager_delegate::<Handler>();
818        assert_is_delegate::<Handler>();
819        assert_is_popup_delegate::<Handler>();
820        assert_is_positioner_delegate::<Handler>();
821    }
822}