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

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