Skip to main content

ferroui_base/input/text_input/
text_input_method_connection.rs

1use super::{
2    TextInputMethodClient, TextInputMethodClientEvents, TextInputMethodEdit, TextInputMethodState,
3};
4use crate::input::raw::RawTextInputEventArgs;
5use crate::input::{IInputDevice, InputElement, InputManager, InputMethod, KeyboardDevice};
6use crate::{Ref, WeakRef};
7use std::cell::Cell;
8use std::fmt;
9use std::rc::{Rc, Weak};
10
11/// Failure of a platform input operation. A rejected commit retains its
12/// accepted preview so that the platform can retry or explicitly cancel it.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum TextInputMethodError {
16    Unavailable,
17    Unsupported,
18    StaleConnection,
19    StateChanged,
20    ReadOnly,
21    ReentrantEdit,
22    Rejected,
23    Intercepted,
24    InvalidRange,
25}
26
27impl fmt::Display for TextInputMethodError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.write_str(match self {
30            Self::Unavailable => "The input owner or input services are unavailable.",
31            Self::Unsupported => "The input client does not support this operation.",
32            Self::StaleConnection => "The input connection or composition has been superseded.",
33            Self::StateChanged => "The input client's captured state is no longer current.",
34            Self::ReadOnly => "The input client is read-only.",
35            Self::ReentrantEdit => "An input edit is already being published.",
36            Self::Rejected => "The input client rejected the edit.",
37            Self::Intercepted => "The input event was intercepted before its owner accepted it.",
38            Self::InvalidRange => {
39                "An input range is outside the text or splits a UTF-16 surrogate pair."
40            }
41        })
42    }
43}
44
45impl std::error::Error for TextInputMethodError {}
46
47/// Optional atomic composition capability of a text input client. Starting a
48/// composition preserves selected text; only an accepted update creates a
49/// preview. Commits use the normal raw and routed text-input pipeline.
50pub trait TextInputMethodEditing {
51    fn begin_composition(&self) -> Result<(), TextInputMethodError>;
52    fn update_composition(
53        &self,
54        text: &str,
55        cursor: Option<i32>,
56    ) -> Result<(), TextInputMethodError>;
57    fn cancel_composition(&self) -> Result<bool, TextInputMethodError>;
58
59    /// Exact range edits are optional; legacy preedit clients remain supported.
60    fn supports_exact_edits(&self) -> bool {
61        false
62    }
63    fn edit_state(&self) -> Result<TextInputMethodState, TextInputMethodError> {
64        Err(TextInputMethodError::Unsupported)
65    }
66    fn apply_edit(
67        &self,
68        _edit: &TextInputMethodEdit,
69        _composing: bool,
70    ) -> Result<bool, TextInputMethodError> {
71        Err(TextInputMethodError::Unsupported)
72    }
73    fn complete_composition(&self) -> Result<bool, TextInputMethodError> {
74        Err(TextInputMethodError::Unsupported)
75    }
76    fn select_exact(&self, _selection: super::TextSelection) -> Result<(), TextInputMethodError> {
77        Err(TextInputMethodError::Unsupported)
78    }
79}
80
81/// A manager-owned connection to one semantic input owner. Platform adapters
82/// capture it when the client is installed. It never redirects to a newer
83/// focused element, and retaining it does not retain its owner or client.
84///
85/// The input method manager creates a connection only for a client that opts
86/// in by implementing [`TextInputMethodEditing`]
87/// ([`TextInputMethodClient::input_method_editing`] returns `Some`); for every
88/// other client [`TextInputMethodClient::input_method_connection`] is `None` and
89/// the platform backends keep their upstream behaviour
90/// (`docs/porting/winui-rich-text.md`, decision D5).
91pub struct TextInputMethodConnection {
92    owner: WeakRef<InputElement>,
93    client: Weak<dyn TextInputMethodClient>,
94    active: Cell<bool>,
95    composition: Cell<u64>,
96}
97
98#[derive(Clone, Copy, PartialEq, Eq)]
99struct ClientStateVersion {
100    events: u64,
101    intrinsic: Option<u64>,
102    composition: u64,
103}
104
105impl TextInputMethodConnection {
106    pub(crate) fn new(
107        owner: &Ref<InputElement>,
108        client: &Rc<dyn TextInputMethodClient>,
109    ) -> Rc<Self> {
110        Rc::new(Self {
111            owner: owner.downgrade(),
112            client: Rc::downgrade(client),
113            active: Cell::new(true),
114            composition: Cell::new(0),
115        })
116    }
117
118    pub(crate) fn invalidate(&self) {
119        self.active.set(false);
120        self.composition.set(self.composition.get().wrapping_add(1));
121    }
122
123    /// The original input owner, only while this connection remains valid.
124    pub fn owner(&self) -> Option<Ref<InputElement>> {
125        self.active
126            .get()
127            .then(|| self.owner.upgrade())
128            .flatten()
129            .filter(|owner| {
130                owner.is_attached_to_visual_tree()
131                    && owner.is_effectively_enabled()
132                    && InputMethod::get_is_input_method_enabled(owner)
133            })
134    }
135
136    pub fn is_current(&self) -> bool {
137        self.owner().is_some() && self.client.upgrade().is_some()
138    }
139
140    /// The input scopes of the owner (`TextInputOptions.InputScopeNames`), while this
141    /// connection remains valid. Backends read them here within 0.4, where they are not
142    /// a field of `TextInputOptions` (decision D8).
143    pub fn input_scope_names(&self) -> Option<Rc<[crate::input::InputScopeNameValue]>> {
144        self.owner().and_then(|owner| super::TextInputOptions::get_input_scope_names(&owner))
145    }
146
147    /// The first input scope of the owner, for a platform with a single hint.
148    pub fn effective_input_scope(&self) -> Option<crate::input::InputScopeNameValue> {
149        self.input_scope_names().and_then(|names| names.first().copied())
150    }
151
152    /// The content type that best matches the first input scope of the owner, or
153    /// `options.content_type` without one.
154    pub fn effective_content_type(&self, options: &super::TextInputOptions) -> super::TextInputContentType {
155        self.effective_input_scope()
156            .map_or(options.content_type, crate::input::InputScopeNameValue::content_type)
157    }
158
159    fn client(&self) -> Result<Rc<dyn TextInputMethodClient>, TextInputMethodError> {
160        if !self.is_current() {
161            return Err(TextInputMethodError::StaleConnection);
162        }
163        self.client
164            .upgrade()
165            .ok_or(TextInputMethodError::StaleConnection)
166    }
167
168    fn state_version(
169        &self,
170        client: &dyn TextInputMethodClient,
171    ) -> Result<ClientStateVersion, TextInputMethodError> {
172        if !self.is_current() {
173            return Err(TextInputMethodError::StaleConnection);
174        }
175        let composition = self.composition.get();
176        let events = client.events();
177        let version = events.state_version();
178        let intrinsic = client.input_method_state_revision();
179        // A custom client's getter can publish events, replace the connection,
180        // or synchronously start another composition. Never capture that newer
181        // state as though it belonged to the operation that called the getter.
182        if !self.is_current() || self.composition.get() != composition {
183            return Err(TextInputMethodError::StaleConnection);
184        }
185        if events.state_version() != version {
186            return Err(TextInputMethodError::StateChanged);
187        }
188        Ok(ClientStateVersion {
189            events: version,
190            intrinsic,
191            composition,
192        })
193    }
194
195    fn check_state(
196        &self,
197        client: &dyn TextInputMethodClient,
198        expected: ClientStateVersion,
199    ) -> Result<(), TextInputMethodError> {
200        if self.state_version(client)? != expected {
201            return Err(TextInputMethodError::StateChanged);
202        }
203        Ok(())
204    }
205
206    pub fn supports_composition(&self) -> bool {
207        self.client()
208            .is_ok_and(|client| client.input_method_editing().is_some())
209    }
210
211    pub fn supports_exact_edits(&self) -> bool {
212        self.client().is_ok_and(|client| {
213            client
214                .input_method_editing()
215                .is_some_and(TextInputMethodEditing::supports_exact_edits)
216        })
217    }
218
219    pub fn edit_state(&self) -> Result<TextInputMethodState, TextInputMethodError> {
220        let client = self.client()?;
221        client
222            .input_method_editing()
223            .ok_or(TextInputMethodError::Unsupported)?
224            .edit_state()
225    }
226
227    /// Applies a committed exact edit through the same raw/routed interception
228    /// path as ordinary text. The owner's acknowledgement is authoritative.
229    pub fn apply_edit(
230        self: &Rc<Self>,
231        edit: &TextInputMethodEdit,
232        timestamp: u64,
233    ) -> Result<bool, TextInputMethodError> {
234        let client = self.client()?;
235        let version = self.state_version(&*client)?;
236        let editing = client
237            .input_method_editing()
238            .ok_or(TextInputMethodError::Unsupported)?;
239        if !editing.supports_exact_edits() {
240            return Err(TextInputMethodError::Unsupported);
241        }
242        edit.apply_to(&editing.edit_state()?.text)?;
243        self.check_state(&*client, version)?;
244        self.dispatch(
245            text_for_edit(edit),
246            timestamp,
247            version,
248            CommitAction::Edit(edit.clone()),
249        )
250    }
251
252    pub fn select_exact(
253        &self,
254        selection: super::TextSelection,
255    ) -> Result<(), TextInputMethodError> {
256        let client = self.client()?;
257        client
258            .input_method_editing()
259            .ok_or(TextInputMethodError::Unsupported)?
260            .select_exact(selection)?;
261        self.client()?;
262        Ok(())
263    }
264
265    /// Starts a new immutable platform session without deleting the current
266    /// selection. Any previous session on this connection is superseded.
267    pub fn begin_composition(
268        self: &Rc<Self>,
269    ) -> Result<TextInputMethodComposition, TextInputMethodError> {
270        let client = self.client()?;
271        let editing = client
272            .input_method_editing()
273            .ok_or(TextInputMethodError::Unsupported)?;
274        let id = self.composition.get().wrapping_add(1);
275        self.composition.set(id);
276        editing.begin_composition()?;
277        let version = self.state_version(&*client)?;
278        if !self.is_current() || self.composition.get() != id {
279            return Err(TextInputMethodError::StaleConnection);
280        }
281        Ok(TextInputMethodComposition(Rc::new(CompositionState {
282            connection: self.clone(),
283            id,
284            version: Cell::new(version),
285            finished: Cell::new(false),
286        })))
287    }
288
289    /// Sends ordinary committed input to a client with the atomic editing
290    /// capability. A platform composing text should use its captured
291    /// composition's `commit` instead.
292    pub fn commit_text(
293        self: &Rc<Self>,
294        text: &str,
295        timestamp: u64,
296    ) -> Result<bool, TextInputMethodError> {
297        let client = self.client()?;
298        client
299            .input_method_editing()
300            .ok_or(TextInputMethodError::Unsupported)?;
301        self.dispatch_commit(text, timestamp, self.state_version(&*client)?)
302    }
303
304    fn dispatch_commit(
305        self: &Rc<Self>,
306        text: &str,
307        timestamp: u64,
308        version: ClientStateVersion,
309    ) -> Result<bool, TextInputMethodError> {
310        self.dispatch(text, timestamp, version, CommitAction::Text)
311    }
312
313    fn dispatch(
314        self: &Rc<Self>,
315        text: &str,
316        timestamp: u64,
317        version: ClientStateVersion,
318        action: CommitAction,
319    ) -> Result<bool, TextInputMethodError> {
320        let owner = self.owner().ok_or(TextInputMethodError::StaleConnection)?;
321        let root = owner
322            .get_input_root()
323            .ok_or(TextInputMethodError::Unavailable)?;
324        let keyboard = KeyboardDevice::instance().ok_or(TextInputMethodError::Unavailable)?;
325        let manager = InputManager::instance().ok_or(TextInputMethodError::Unavailable)?;
326        let commit = Rc::new(TextInputMethodCommit {
327            connection: self.clone(),
328            version,
329            result: Cell::new(None),
330            action,
331        });
332        let device: Rc<dyn IInputDevice> = keyboard;
333        let event = Rc::new(RawTextInputEventArgs::for_input_method(
334            device,
335            timestamp,
336            root,
337            text,
338            commit.clone(),
339        ));
340        manager.process_input(event);
341        commit
342            .result
343            .get()
344            .unwrap_or(Err(TextInputMethodError::Intercepted))
345    }
346}
347
348/// The accepted input request attached to raw and routed text input. The owner
349/// checks `is_current` immediately before editing, then acknowledges the real
350/// result; an application handling the event does not imply an accepted edit.
351pub struct TextInputMethodCommit {
352    connection: Rc<TextInputMethodConnection>,
353    version: ClientStateVersion,
354    result: Cell<Option<Result<bool, TextInputMethodError>>>,
355    action: CommitAction,
356}
357
358enum CommitAction {
359    Text,
360    Edit(TextInputMethodEdit),
361    CompleteComposition,
362}
363
364fn text_for_edit(edit: &TextInputMethodEdit) -> &str {
365    &edit.text
366}
367
368impl TextInputMethodCommit {
369    pub fn edit(&self) -> Option<&TextInputMethodEdit> {
370        match &self.action {
371            CommitAction::Edit(edit) => Some(edit),
372            _ => None,
373        }
374    }
375    pub fn completes_composition(&self) -> bool {
376        matches!(self.action, CommitAction::CompleteComposition)
377    }
378    pub fn owner(&self) -> Option<Ref<InputElement>> {
379        self.connection.owner()
380    }
381
382    pub fn is_current(&self) -> bool {
383        self.connection
384            .client()
385            .is_ok_and(|client| self.connection.check_state(&*client, self.version).is_ok())
386    }
387
388    pub fn complete(&self, result: Result<bool, TextInputMethodError>) {
389        if self.result.get().is_none() {
390            self.result.set(Some(result));
391        }
392    }
393}
394
395struct CompositionState {
396    connection: Rc<TextInputMethodConnection>,
397    id: u64,
398    version: Cell<ClientStateVersion>,
399    finished: Cell<bool>,
400}
401
402/// A platform composition bound to one connection and one accepted client state.
403/// Clones refer to the same session. Late callbacks fail after reset, focus
404/// handoff, external edits, or a newer native composition.
405#[derive(Clone)]
406pub struct TextInputMethodComposition(Rc<CompositionState>);
407
408impl TextInputMethodComposition {
409    fn check_lifetime(&self) -> Result<(), TextInputMethodError> {
410        if self.0.finished.get() || self.0.connection.composition.get() != self.0.id {
411            return Err(TextInputMethodError::StaleConnection);
412        }
413        Ok(())
414    }
415
416    fn client(&self) -> Result<Rc<dyn TextInputMethodClient>, TextInputMethodError> {
417        self.check_lifetime()?;
418        let client = self.0.connection.client()?;
419        let version = self.0.version.get();
420        self.0.connection.check_state(&*client, version)?;
421        self.check_lifetime()?;
422        if self.0.version.get() != version {
423            return Err(TextInputMethodError::StateChanged);
424        }
425        Ok(client)
426    }
427
428    pub fn is_current(&self) -> bool {
429        self.client().is_ok()
430    }
431
432    fn finish_operation(
433        &self,
434        client: &dyn TextInputMethodClient,
435    ) -> Result<(), TextInputMethodError> {
436        self.check_lifetime()?;
437        let version = self.0.connection.state_version(client)?;
438        self.check_lifetime()?;
439        self.0.version.set(version);
440        Ok(())
441    }
442
443    pub fn update(&self, text: &str, cursor: Option<i32>) -> Result<(), TextInputMethodError> {
444        let client = self.client()?;
445        client
446            .input_method_editing()
447            .ok_or(TextInputMethodError::Unsupported)?
448            .update_composition(text, cursor)?;
449        self.finish_operation(&*client)
450    }
451
452    /// Applies the platform's delta to the current preview without committing
453    /// history. The resulting selection is part of this operation.
454    pub fn apply_edit(&self, edit: &TextInputMethodEdit) -> Result<bool, TextInputMethodError> {
455        let client = self.client()?;
456        let result = client
457            .input_method_editing()
458            .ok_or(TextInputMethodError::Unsupported)?
459            .apply_edit(edit, true)?;
460        self.finish_operation(&*client)?;
461        Ok(result)
462    }
463
464    pub fn select_exact(
465        &self,
466        selection: super::TextSelection,
467    ) -> Result<(), TextInputMethodError> {
468        let client = self.client()?;
469        client
470            .input_method_editing()
471            .ok_or(TextInputMethodError::Unsupported)?
472            .select_exact(selection)?;
473        self.finish_operation(&*client)
474    }
475
476    /// Commits exactly the accepted preview, including empty deletions and
477    /// arbitrary directed selection, as one undo transaction.
478    pub fn complete(&self, timestamp: u64) -> Result<bool, TextInputMethodError> {
479        let client = self.client()?;
480        let result = self.0.connection.dispatch(
481            "",
482            timestamp,
483            self.0.version.get(),
484            CommitAction::CompleteComposition,
485        )?;
486        self.finish_operation(&*client)?;
487        self.0.finished.set(true);
488        Ok(result)
489    }
490
491    pub fn commit(&self, text: &str, timestamp: u64) -> Result<bool, TextInputMethodError> {
492        let client = self.client()?;
493        let result = self
494            .0
495            .connection
496            .dispatch_commit(text, timestamp, self.0.version.get())?;
497        self.finish_operation(&*client)?;
498        self.0.finished.set(true);
499        Ok(result)
500    }
501
502    pub fn cancel(&self) -> Result<bool, TextInputMethodError> {
503        let client = self.client()?;
504        let result = client
505            .input_method_editing()
506            .ok_or(TextInputMethodError::Unsupported)?
507            .cancel_composition()?;
508        self.finish_operation(&*client)?;
509        self.0.finished.set(true);
510        Ok(result)
511    }
512}
513
514impl TextInputMethodClientEvents {
515    pub(crate) fn set_connection(&self, connection: Option<&Rc<TextInputMethodConnection>>) {
516        *self.connection.borrow_mut() = connection.map(Rc::downgrade);
517    }
518
519    pub(crate) fn connection(&self) -> Option<Rc<TextInputMethodConnection>> {
520        self.connection.borrow().as_ref().and_then(Weak::upgrade)
521    }
522}