| 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 | |