1 | use std::ops::Deref; |
2 | |
3 | use sctk::globals::GlobalData; |
4 | use sctk::reexports::client::{Connection, Proxy, QueueHandle}; |
5 | |
6 | use sctk::reexports::client::globals::{BindError, GlobalList}; |
7 | use sctk::reexports::client::protocol::wl_surface::WlSurface; |
8 | use sctk::reexports::client::{delegate_dispatch, Dispatch}; |
9 | use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; |
10 | use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ |
11 | ContentHint, ContentPurpose, Event as TextInputEvent, ZwpTextInputV3, |
12 | }; |
13 | |
14 | use crate::event::{Ime, WindowEvent}; |
15 | use crate::platform_impl::wayland; |
16 | use crate::platform_impl::wayland::state::WinitState; |
17 | use crate::window::ImePurpose; |
18 | |
19 | pub struct TextInputState { |
20 | text_input_manager: ZwpTextInputManagerV3, |
21 | } |
22 | |
23 | impl TextInputState { |
24 | pub fn new( |
25 | globals: &GlobalList, |
26 | queue_handle: &QueueHandle<WinitState>, |
27 | ) -> Result<Self, BindError> { |
28 | let text_input_manager: as Try>::Output = globals.bind(qh:queue_handle, version:1..=1, udata:GlobalData)?; |
29 | Ok(Self { text_input_manager }) |
30 | } |
31 | } |
32 | |
33 | impl Deref for TextInputState { |
34 | type Target = ZwpTextInputManagerV3; |
35 | |
36 | fn deref(&self) -> &Self::Target { |
37 | &self.text_input_manager |
38 | } |
39 | } |
40 | |
41 | impl 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 | |
53 | impl 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 | |
160 | pub trait ZwpTextInputV3Ext { |
161 | fn set_content_type_by_purpose(&self, purpose: ImePurpose); |
162 | } |
163 | |
164 | impl 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)] |
177 | pub struct TextInputData { |
178 | inner: std::sync::Mutex<TextInputDataInner>, |
179 | } |
180 | |
181 | #[derive (Default)] |
182 | pub 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. |
194 | struct Preedit { |
195 | text: String, |
196 | cursor_begin: Option<usize>, |
197 | cursor_end: Option<usize>, |
198 | } |
199 | |
200 | delegate_dispatch!(WinitState: [ZwpTextInputManagerV3: GlobalData] => TextInputState); |
201 | delegate_dispatch!(WinitState: [ZwpTextInputV3: TextInputData] => TextInputState); |
202 | |