frust_core/event/ime.rs
1//! IME vocabulary: composition events, the content-type hint, and the published IME surface.
2
3use std::fmt;
4
5use kurbo::Rect;
6
7use super::key::EditingState;
8
9/// An IME event, delivered down the focus path. Desktop sends `Compose`/`Commit`, mobile bridges
10/// sync whole values (`ApplyEditingState`), and `Enabled`/`Disabled` bracket a session.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub enum ImeEvent {
13 /// Preedit (marked) text; `cursor` is a byte-indexed selection inside it, as winit reports.
14 Compose {
15 text: String,
16 cursor: Option<(usize, usize)>,
17 },
18 /// Insert the finished composition and clear the composing region.
19 Commit(String),
20 /// Replace the whole editing state (mobile state-sync path).
21 ApplyEditingState(EditingState),
22 Enabled,
23 Disabled,
24}
25
26/// What a focused editable holds — the hint a shell maps onto its platform IME. Masking alone
27/// does not stop a soft keyboard showing or learning the text; only this hint does. Shells branch
28/// security on [`is_secret`](Self::is_secret), never on a `_ =>` arm (the enum is non-exhaustive).
29///
30/// # Platform mapping
31/// | Variant | Android (`InputType` / `EditorInfo.imeOptions`) | iOS (`UITextInputTraits`) | Desktop (winit) |
32/// |---|---|---|---|
33/// | [`Normal`](Self::Normal) | `TYPE_CLASS_TEXT` | platform defaults | `ImePurpose::Normal` |
34/// | [`Password`](Self::Password) | `TYPE_CLASS_TEXT \| TYPE_TEXT_VARIATION_PASSWORD`, plus `TYPE_TEXT_FLAG_NO_SUGGESTIONS` and `IME_FLAG_NO_PERSONALIZED_LEARNING` | `isSecureTextEntry = true`, `textContentType = .password`, `autocorrectionType = .no`, `spellCheckingType = .no`, plus smart-punctuation suppression (see [`Terminal`](Self::Terminal)) | `ImePurpose::Password` |
35/// | [`NoSuggestions`](Self::NoSuggestions) | `TYPE_CLASS_TEXT \| TYPE_TEXT_FLAG_NO_SUGGESTIONS`, plus `IME_FLAG_NO_PERSONALIZED_LEARNING` | `autocorrectionType = .no`, `spellCheckingType = .no`, plus smart-punctuation suppression | no equivalent — `ImePurpose::Normal` |
36/// | [`Terminal`](Self::Terminal) | `TYPE_CLASS_TEXT \| TYPE_TEXT_FLAG_NO_SUGGESTIONS`, plus `IME_FLAG_NO_PERSONALIZED_LEARNING` (same as `NoSuggestions`) | `isSecureTextEntry = false`, `autocorrectionType = .no`, `spellCheckingType = .no`, `smartQuotesType = .no`, `smartDashesType = .no`, `smartInsertDeleteType = .no`, `autocapitalizationType = .none`, `textContentType = nil` | `ImePurpose::Terminal` |
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum ImeContentType {
40 /// No hint: platform defaults. What every field publishes unless it opts in.
41 #[default]
42 Normal,
43 /// Secret text: secure entry, with suggestions and personalized learning suppressed.
44 Password,
45 /// Non-secret text never suggested, autocorrected or learned; no secure entry.
46 NoSuggestions,
47 /// Byte-exact entry: `NoSuggestions` plus no smart punctuation or autocapitalization; unmasked.
48 Terminal,
49}
50
51impl ImeContentType {
52 /// Whether the platform must treat the field as secret (secure entry). Both predicates match
53 /// exhaustively, so a new variant fails to compile until it is classified.
54 pub fn is_secret(self) -> bool {
55 match self {
56 Self::Password => true,
57 Self::Normal | Self::NoSuggestions | Self::Terminal => false,
58 }
59 }
60
61 /// Whether the platform must suppress suggestions, autocorrect and word learning.
62 pub fn suppresses_suggestions(self) -> bool {
63 match self {
64 Self::Password | Self::NoSuggestions | Self::Terminal => true,
65 Self::Normal => false,
66 }
67 }
68}
69
70/// The IME surface a focused editable publishes through `EventCtx::publish_ime_state`; the shell
71/// reads it from [`crate::app::RenderRoot::ime_state`]. `editing` is plaintext even for a secret
72/// field (the platform mirror needs it); [`fmt::Debug`] redacts it then.
73#[derive(Clone, PartialEq)]
74pub struct ImeState {
75 /// Whether the focused widget wants IME active.
76 pub active: bool,
77 pub editing: EditingState,
78 /// The caret rectangle in logical coordinates, for candidate placement.
79 pub caret: Option<Rect>,
80 pub content_type: ImeContentType,
81 /// Keep the input surface wired (copy still needs it) without raising an on-screen keyboard.
82 pub suppress_soft_keyboard: bool,
83}
84
85impl Default for ImeState {
86 fn default() -> Self {
87 Self {
88 active: false,
89 editing: EditingState::default(),
90 caret: None,
91 content_type: ImeContentType::Normal,
92 suppress_soft_keyboard: false,
93 }
94 }
95}
96
97impl fmt::Debug for ImeState {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 struct Redacted<'a>(&'a EditingState);
100 impl fmt::Debug for Redacted<'_> {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.debug_struct("EditingState")
103 .field("text", &"<redacted>")
104 .field("selection_base", &self.0.selection_base)
105 .field("selection_extent", &self.0.selection_extent)
106 .field("composing_base", &self.0.composing_base)
107 .field("composing_extent", &self.0.composing_extent)
108 .finish()
109 }
110 }
111
112 let mut s = f.debug_struct("ImeState");
113 s.field("active", &self.active);
114 if self.content_type.is_secret() {
115 s.field("editing", &Redacted(&self.editing));
116 } else {
117 s.field("editing", &self.editing);
118 }
119 s.field("caret", &self.caret)
120 .field("content_type", &self.content_type)
121 .field("suppress_soft_keyboard", &self.suppress_soft_keyboard)
122 .finish()
123 }
124}