winit/platform_impl/linux/wayland/seat/text_input/
mod.rs

1use std::ops::Deref;
2
3use sctk::globals::GlobalData;
4use sctk::reexports::client::globals::{BindError, GlobalList};
5use sctk::reexports::client::protocol::wl_surface::WlSurface;
6use sctk::reexports::client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle};
7use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3;
8use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::{
9    ContentHint, ContentPurpose, Event as TextInputEvent, ZwpTextInputV3,
10};
11
12use crate::event::{Ime, WindowEvent};
13use crate::platform_impl::wayland;
14use crate::platform_impl::wayland::state::WinitState;
15use crate::window::ImePurpose;
16
17pub struct TextInputState {
18    text_input_manager: ZwpTextInputManagerV3,
19}
20
21impl TextInputState {
22    pub fn new(
23        globals: &GlobalList,
24        queue_handle: &QueueHandle<WinitState>,
25    ) -> Result<Self, BindError> {
26        let text_input_manager = globals.bind(queue_handle, 1..=1, GlobalData)?;
27        Ok(Self { text_input_manager })
28    }
29}
30
31impl Deref for TextInputState {
32    type Target = ZwpTextInputManagerV3;
33
34    fn deref(&self) -> &Self::Target {
35        &self.text_input_manager
36    }
37}
38
39impl Dispatch<ZwpTextInputManagerV3, GlobalData, WinitState> for TextInputState {
40    fn event(
41        _state: &mut WinitState,
42        _proxy: &ZwpTextInputManagerV3,
43        _event: <ZwpTextInputManagerV3 as Proxy>::Event,
44        _data: &GlobalData,
45        _conn: &Connection,
46        _qhandle: &QueueHandle<WinitState>,
47    ) {
48    }
49}
50
51impl Dispatch<ZwpTextInputV3, TextInputData, WinitState> for TextInputState {
52    fn event(
53        state: &mut WinitState,
54        text_input: &ZwpTextInputV3,
55        event: <ZwpTextInputV3 as Proxy>::Event,
56        data: &TextInputData,
57        _conn: &Connection,
58        _qhandle: &QueueHandle<WinitState>,
59    ) {
60        let windows = state.windows.get_mut();
61        let mut text_input_data = data.inner.lock().unwrap();
62        match event {
63            TextInputEvent::Enter { surface } => {
64                let window_id = wayland::make_wid(&surface);
65                text_input_data.surface = Some(surface);
66
67                let mut window = match windows.get(&window_id) {
68                    Some(window) => window.lock().unwrap(),
69                    None => return,
70                };
71
72                if window.ime_allowed() {
73                    text_input.enable();
74                    text_input.set_content_type_by_purpose(window.ime_purpose());
75                    text_input.commit();
76                    state.events_sink.push_window_event(WindowEvent::Ime(Ime::Enabled), window_id);
77                }
78
79                window.text_input_entered(text_input);
80            },
81            TextInputEvent::Leave { surface } => {
82                text_input_data.surface = None;
83
84                // Always issue a disable.
85                text_input.disable();
86                text_input.commit();
87
88                let window_id = wayland::make_wid(&surface);
89
90                // XXX this check is essential, because `leave` could have a
91                // reference to nil surface...
92                let mut window = match windows.get(&window_id) {
93                    Some(window) => window.lock().unwrap(),
94                    None => return,
95                };
96
97                window.text_input_left(text_input);
98
99                state.events_sink.push_window_event(WindowEvent::Ime(Ime::Disabled), window_id);
100            },
101            TextInputEvent::PreeditString { text, cursor_begin, cursor_end } => {
102                let text = text.unwrap_or_default();
103                let cursor_begin = usize::try_from(cursor_begin)
104                    .ok()
105                    .and_then(|idx| text.is_char_boundary(idx).then_some(idx));
106                let cursor_end = usize::try_from(cursor_end)
107                    .ok()
108                    .and_then(|idx| text.is_char_boundary(idx).then_some(idx));
109
110                text_input_data.pending_preedit = Some(Preedit { text, cursor_begin, cursor_end })
111            },
112            TextInputEvent::CommitString { text } => {
113                text_input_data.pending_preedit = None;
114                text_input_data.pending_commit = text;
115            },
116            TextInputEvent::Done { .. } => {
117                let window_id = match text_input_data.surface.as_ref() {
118                    Some(surface) => wayland::make_wid(surface),
119                    None => return,
120                };
121
122                // Clear preedit at the start of `Done`.
123                state.events_sink.push_window_event(
124                    WindowEvent::Ime(Ime::Preedit(String::new(), None)),
125                    window_id,
126                );
127
128                // Send `Commit`.
129                if let Some(text) = text_input_data.pending_commit.take() {
130                    state
131                        .events_sink
132                        .push_window_event(WindowEvent::Ime(Ime::Commit(text)), window_id);
133                }
134
135                // Send preedit.
136                if let Some(preedit) = text_input_data.pending_preedit.take() {
137                    let cursor_range =
138                        preedit.cursor_begin.map(|b| (b, preedit.cursor_end.unwrap_or(b)));
139
140                    state.events_sink.push_window_event(
141                        WindowEvent::Ime(Ime::Preedit(preedit.text, cursor_range)),
142                        window_id,
143                    );
144                }
145            },
146            TextInputEvent::DeleteSurroundingText { .. } => {
147                // Not handled.
148            },
149            _ => {},
150        }
151    }
152}
153
154pub trait ZwpTextInputV3Ext {
155    fn set_content_type_by_purpose(&self, purpose: ImePurpose);
156}
157
158impl ZwpTextInputV3Ext for ZwpTextInputV3 {
159    fn set_content_type_by_purpose(&self, purpose: ImePurpose) {
160        let (hint, purpose) = match purpose {
161            ImePurpose::Normal => (ContentHint::None, ContentPurpose::Normal),
162            ImePurpose::Password => (ContentHint::SensitiveData, ContentPurpose::Password),
163            ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal),
164        };
165        self.set_content_type(hint, purpose);
166    }
167}
168
169/// The Data associated with the text input.
170#[derive(Default)]
171pub struct TextInputData {
172    inner: std::sync::Mutex<TextInputDataInner>,
173}
174
175#[derive(Default)]
176pub struct TextInputDataInner {
177    /// The `WlSurface` we're performing input to.
178    surface: Option<WlSurface>,
179
180    /// The commit to submit on `done`.
181    pending_commit: Option<String>,
182
183    /// The preedit to submit on `done`.
184    pending_preedit: Option<Preedit>,
185}
186
187/// The state of the preedit.
188struct Preedit {
189    text: String,
190    cursor_begin: Option<usize>,
191    cursor_end: Option<usize>,
192}
193
194delegate_dispatch!(WinitState: [ZwpTextInputManagerV3: GlobalData] => TextInputState);
195delegate_dispatch!(WinitState: [ZwpTextInputV3: TextInputData] => TextInputState);