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