Skip to main content

cbf_chrome/data/
ime.rs

1//! Chrome-specific IME (Input Method Editor) text span types and composition state, with conversions to/from `cbf` equivalents.
2
3use super::ids::{PopupId, TabId};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ChromeImeTextSpanType {
7    Composition,
8    Suggestion,
9    MisspellingSuggestion,
10    Autocorrect,
11    GrammarSuggestion,
12}
13
14impl From<ChromeImeTextSpanType> for cbf::data::ime::ImeTextSpanType {
15    fn from(value: ChromeImeTextSpanType) -> Self {
16        match value {
17            ChromeImeTextSpanType::Composition => Self::Composition,
18            ChromeImeTextSpanType::Suggestion => Self::Suggestion,
19            ChromeImeTextSpanType::MisspellingSuggestion => Self::MisspellingSuggestion,
20            ChromeImeTextSpanType::Autocorrect => Self::Autocorrect,
21            ChromeImeTextSpanType::GrammarSuggestion => Self::GrammarSuggestion,
22        }
23    }
24}
25
26impl From<cbf::data::ime::ImeTextSpanType> for ChromeImeTextSpanType {
27    fn from(value: cbf::data::ime::ImeTextSpanType) -> Self {
28        match value {
29            cbf::data::ime::ImeTextSpanType::Composition => Self::Composition,
30            cbf::data::ime::ImeTextSpanType::Suggestion => Self::Suggestion,
31            cbf::data::ime::ImeTextSpanType::MisspellingSuggestion => Self::MisspellingSuggestion,
32            cbf::data::ime::ImeTextSpanType::Autocorrect => Self::Autocorrect,
33            cbf::data::ime::ImeTextSpanType::GrammarSuggestion => Self::GrammarSuggestion,
34        }
35    }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum ChromeImeTextSpanThickness {
40    #[default]
41    None,
42    Thin,
43    Thick,
44}
45
46impl From<ChromeImeTextSpanThickness> for cbf::data::ime::ImeTextSpanThickness {
47    fn from(value: ChromeImeTextSpanThickness) -> Self {
48        match value {
49            ChromeImeTextSpanThickness::None => Self::None,
50            ChromeImeTextSpanThickness::Thin => Self::Thin,
51            ChromeImeTextSpanThickness::Thick => Self::Thick,
52        }
53    }
54}
55
56impl From<cbf::data::ime::ImeTextSpanThickness> for ChromeImeTextSpanThickness {
57    fn from(value: cbf::data::ime::ImeTextSpanThickness) -> Self {
58        match value {
59            cbf::data::ime::ImeTextSpanThickness::None => Self::None,
60            cbf::data::ime::ImeTextSpanThickness::Thin => Self::Thin,
61            cbf::data::ime::ImeTextSpanThickness::Thick => Self::Thick,
62        }
63    }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
67pub enum ChromeImeTextSpanUnderlineStyle {
68    #[default]
69    None,
70    Solid,
71    Dot,
72    Dash,
73    Squiggle,
74}
75
76impl From<ChromeImeTextSpanUnderlineStyle> for cbf::data::ime::ImeTextSpanUnderlineStyle {
77    fn from(value: ChromeImeTextSpanUnderlineStyle) -> Self {
78        match value {
79            ChromeImeTextSpanUnderlineStyle::None => Self::None,
80            ChromeImeTextSpanUnderlineStyle::Solid => Self::Solid,
81            ChromeImeTextSpanUnderlineStyle::Dot => Self::Dot,
82            ChromeImeTextSpanUnderlineStyle::Dash => Self::Dash,
83            ChromeImeTextSpanUnderlineStyle::Squiggle => Self::Squiggle,
84        }
85    }
86}
87
88impl From<cbf::data::ime::ImeTextSpanUnderlineStyle> for ChromeImeTextSpanUnderlineStyle {
89    fn from(value: cbf::data::ime::ImeTextSpanUnderlineStyle) -> Self {
90        match value {
91            cbf::data::ime::ImeTextSpanUnderlineStyle::None => Self::None,
92            cbf::data::ime::ImeTextSpanUnderlineStyle::Solid => Self::Solid,
93            cbf::data::ime::ImeTextSpanUnderlineStyle::Dot => Self::Dot,
94            cbf::data::ime::ImeTextSpanUnderlineStyle::Dash => Self::Dash,
95            cbf::data::ime::ImeTextSpanUnderlineStyle::Squiggle => Self::Squiggle,
96        }
97    }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct ChromeImeTextRange {
102    pub start: i32,
103    pub end: i32,
104}
105
106impl From<ChromeImeTextRange> for cbf::data::ime::ImeTextRange {
107    fn from(value: ChromeImeTextRange) -> Self {
108        Self {
109            start: value.start,
110            end: value.end,
111        }
112    }
113}
114
115impl From<cbf::data::ime::ImeTextRange> for ChromeImeTextRange {
116    fn from(value: cbf::data::ime::ImeTextRange) -> Self {
117        Self {
118            start: value.start,
119            end: value.end,
120        }
121    }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Default)]
125pub struct ChromeImeTextSpanStyle {
126    pub underline_color: u32,
127    pub thickness: ChromeImeTextSpanThickness,
128    pub underline_style: ChromeImeTextSpanUnderlineStyle,
129    pub text_color: u32,
130    pub background_color: u32,
131    pub suggestion_highlight_color: u32,
132    pub remove_on_finish_composing: bool,
133    pub interim_char_selection: bool,
134    pub should_hide_suggestion_menu: bool,
135}
136
137impl From<ChromeImeTextSpanStyle> for cbf::data::ime::ImeTextSpanStyle {
138    fn from(value: ChromeImeTextSpanStyle) -> Self {
139        Self {
140            underline_color: value.underline_color,
141            thickness: value.thickness.into(),
142            underline_style: value.underline_style.into(),
143            text_color: value.text_color,
144            background_color: value.background_color,
145            suggestion_highlight_color: value.suggestion_highlight_color,
146            remove_on_finish_composing: value.remove_on_finish_composing,
147            interim_char_selection: value.interim_char_selection,
148            should_hide_suggestion_menu: value.should_hide_suggestion_menu,
149        }
150    }
151}
152
153impl From<cbf::data::ime::ImeTextSpanStyle> for ChromeImeTextSpanStyle {
154    fn from(value: cbf::data::ime::ImeTextSpanStyle) -> Self {
155        Self {
156            underline_color: value.underline_color,
157            thickness: value.thickness.into(),
158            underline_style: value.underline_style.into(),
159            text_color: value.text_color,
160            background_color: value.background_color,
161            suggestion_highlight_color: value.suggestion_highlight_color,
162            remove_on_finish_composing: value.remove_on_finish_composing,
163            interim_char_selection: value.interim_char_selection,
164            should_hide_suggestion_menu: value.should_hide_suggestion_menu,
165        }
166    }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct ChromeImeTextSpan {
171    pub r#type: ChromeImeTextSpanType,
172    pub start_offset: u32,
173    pub end_offset: u32,
174    pub chrome_style: Option<ChromeImeTextSpanStyle>,
175}
176
177impl ChromeImeTextSpan {
178    pub fn new(r#type: ChromeImeTextSpanType, start_offset: u32, end_offset: u32) -> Self {
179        Self {
180            r#type,
181            start_offset,
182            end_offset,
183            chrome_style: None,
184        }
185    }
186
187    pub fn with_chrome_style(mut self, chrome_style: ChromeImeTextSpanStyle) -> Self {
188        self.chrome_style = Some(chrome_style);
189        self
190    }
191
192    pub fn no_decoration(
193        r#type: ChromeImeTextSpanType,
194        start_offset: u32,
195        end_offset: u32,
196    ) -> Self {
197        Self {
198            r#type,
199            start_offset,
200            end_offset,
201            chrome_style: Some(ChromeImeTextSpanStyle::default()),
202        }
203    }
204}
205
206impl From<ChromeImeTextSpan> for cbf::data::ime::ImeTextSpan {
207    fn from(value: ChromeImeTextSpan) -> Self {
208        Self {
209            r#type: value.r#type.into(),
210            start_offset: value.start_offset,
211            end_offset: value.end_offset,
212            style: value.chrome_style.map(Into::into),
213        }
214    }
215}
216
217impl From<cbf::data::ime::ImeTextSpan> for ChromeImeTextSpan {
218    fn from(value: cbf::data::ime::ImeTextSpan) -> Self {
219        Self {
220            r#type: value.r#type.into(),
221            start_offset: value.start_offset,
222            end_offset: value.end_offset,
223            chrome_style: value.style.map(Into::into),
224        }
225    }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct ChromeImeComposition {
230    pub browsing_context_id: TabId,
231    pub text: String,
232    pub selection_start: i32,
233    pub selection_end: i32,
234    pub replacement_range: Option<ChromeImeTextRange>,
235    pub spans: Vec<ChromeImeTextSpan>,
236}
237
238impl From<ChromeImeComposition> for cbf::data::ime::ImeComposition {
239    fn from(value: ChromeImeComposition) -> Self {
240        Self {
241            browsing_context_id: value.browsing_context_id.into(),
242            text: value.text,
243            selection_start: value.selection_start,
244            selection_end: value.selection_end,
245            replacement_range: value.replacement_range.map(Into::into),
246            spans: value.spans.into_iter().map(Into::into).collect(),
247        }
248    }
249}
250
251impl From<cbf::data::ime::ImeComposition> for ChromeImeComposition {
252    fn from(value: cbf::data::ime::ImeComposition) -> Self {
253        Self {
254            browsing_context_id: value.browsing_context_id.into(),
255            text: value.text,
256            selection_start: value.selection_start,
257            selection_end: value.selection_end,
258            replacement_range: value.replacement_range.map(Into::into),
259            spans: value.spans.into_iter().map(Into::into).collect(),
260        }
261    }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct ChromeImeCommitText {
266    pub browsing_context_id: TabId,
267    pub text: String,
268    pub relative_caret_position: i32,
269    pub replacement_range: Option<ChromeImeTextRange>,
270    pub spans: Vec<ChromeImeTextSpan>,
271}
272
273impl From<ChromeImeCommitText> for cbf::data::ime::ImeCommitText {
274    fn from(value: ChromeImeCommitText) -> Self {
275        Self {
276            browsing_context_id: value.browsing_context_id.into(),
277            text: value.text,
278            relative_caret_position: value.relative_caret_position,
279            replacement_range: value.replacement_range.map(Into::into),
280            spans: value.spans.into_iter().map(Into::into).collect(),
281        }
282    }
283}
284
285impl From<cbf::data::ime::ImeCommitText> for ChromeImeCommitText {
286    fn from(value: cbf::data::ime::ImeCommitText) -> Self {
287        Self {
288            browsing_context_id: value.browsing_context_id.into(),
289            text: value.text,
290            relative_caret_position: value.relative_caret_position,
291            replacement_range: value.replacement_range.map(Into::into),
292            spans: value.spans.into_iter().map(Into::into).collect(),
293        }
294    }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct ChromeTransientImeComposition {
299    pub popup_id: PopupId,
300    pub text: String,
301    pub selection_start: i32,
302    pub selection_end: i32,
303    pub replacement_range: Option<ChromeImeTextRange>,
304    pub spans: Vec<ChromeImeTextSpan>,
305}
306
307impl From<cbf::data::transient_browsing_context::TransientImeComposition>
308    for ChromeTransientImeComposition
309{
310    fn from(value: cbf::data::transient_browsing_context::TransientImeComposition) -> Self {
311        Self {
312            popup_id: value.transient_browsing_context_id.into(),
313            text: value.text,
314            selection_start: value.selection_start,
315            selection_end: value.selection_end,
316            replacement_range: value.replacement_range.map(Into::into),
317            spans: value.spans.into_iter().map(Into::into).collect(),
318        }
319    }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct ChromeTransientImeCommitText {
324    pub popup_id: PopupId,
325    pub text: String,
326    pub relative_caret_position: i32,
327    pub replacement_range: Option<ChromeImeTextRange>,
328    pub spans: Vec<ChromeImeTextSpan>,
329}
330
331impl From<cbf::data::transient_browsing_context::TransientImeCommitText>
332    for ChromeTransientImeCommitText
333{
334    fn from(value: cbf::data::transient_browsing_context::TransientImeCommitText) -> Self {
335        Self {
336            popup_id: value.transient_browsing_context_id.into(),
337            text: value.text,
338            relative_caret_position: value.relative_caret_position,
339            replacement_range: value.replacement_range.map(Into::into),
340            spans: value.spans.into_iter().map(Into::into).collect(),
341        }
342    }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub enum ChromeConfirmCompositionBehavior {
347    DoNotKeepSelection,
348    KeepSelection,
349}
350
351impl From<ChromeConfirmCompositionBehavior> for cbf::data::ime::ConfirmCompositionBehavior {
352    fn from(value: ChromeConfirmCompositionBehavior) -> Self {
353        match value {
354            ChromeConfirmCompositionBehavior::DoNotKeepSelection => Self::DoNotKeepSelection,
355            ChromeConfirmCompositionBehavior::KeepSelection => Self::KeepSelection,
356        }
357    }
358}
359
360impl From<cbf::data::ime::ConfirmCompositionBehavior> for ChromeConfirmCompositionBehavior {
361    fn from(value: cbf::data::ime::ConfirmCompositionBehavior) -> Self {
362        match value {
363            cbf::data::ime::ConfirmCompositionBehavior::DoNotKeepSelection => {
364                Self::DoNotKeepSelection
365            }
366            cbf::data::ime::ConfirmCompositionBehavior::KeepSelection => Self::KeepSelection,
367        }
368    }
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub struct ChromeImeRect {
373    pub x: i32,
374    pub y: i32,
375    pub width: i32,
376    pub height: i32,
377}
378
379impl From<ChromeImeRect> for cbf::data::ime::ImeRect {
380    fn from(value: ChromeImeRect) -> Self {
381        Self {
382            x: value.x,
383            y: value.y,
384            width: value.width,
385            height: value.height,
386        }
387    }
388}
389
390impl From<cbf::data::ime::ImeRect> for ChromeImeRect {
391    fn from(value: cbf::data::ime::ImeRect) -> Self {
392        Self {
393            x: value.x,
394            y: value.y,
395            width: value.width,
396            height: value.height,
397        }
398    }
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
402pub struct ChromeImeCompositionBounds {
403    pub range_start: i32,
404    pub range_end: i32,
405    pub character_bounds: Vec<ChromeImeRect>,
406}
407
408impl From<ChromeImeCompositionBounds> for cbf::data::ime::ImeCompositionBounds {
409    fn from(value: ChromeImeCompositionBounds) -> Self {
410        Self {
411            range_start: value.range_start,
412            range_end: value.range_end,
413            character_bounds: value.character_bounds.into_iter().map(Into::into).collect(),
414        }
415    }
416}
417
418impl From<cbf::data::ime::ImeCompositionBounds> for ChromeImeCompositionBounds {
419    fn from(value: cbf::data::ime::ImeCompositionBounds) -> Self {
420        Self {
421            range_start: value.range_start,
422            range_end: value.range_end,
423            character_bounds: value.character_bounds.into_iter().map(Into::into).collect(),
424        }
425    }
426}
427
428#[derive(Debug, Clone, PartialEq, Eq)]
429pub struct ChromeTextSelectionBounds {
430    pub range_start: i32,
431    pub range_end: i32,
432    pub caret_rect: ChromeImeRect,
433    pub first_selection_rect: ChromeImeRect,
434}
435
436impl From<ChromeTextSelectionBounds> for cbf::data::ime::TextSelectionBounds {
437    fn from(value: ChromeTextSelectionBounds) -> Self {
438        Self {
439            range_start: value.range_start,
440            range_end: value.range_end,
441            caret_rect: value.caret_rect.into(),
442            first_selection_rect: value.first_selection_rect.into(),
443        }
444    }
445}
446
447impl From<cbf::data::ime::TextSelectionBounds> for ChromeTextSelectionBounds {
448    fn from(value: cbf::data::ime::TextSelectionBounds) -> Self {
449        Self {
450            range_start: value.range_start,
451            range_end: value.range_end,
452            caret_rect: value.caret_rect.into(),
453            first_selection_rect: value.first_selection_rect.into(),
454        }
455    }
456}
457
458#[derive(Debug, Clone, PartialEq, Eq)]
459pub struct ChromeImeBoundsUpdate {
460    pub composition: Option<ChromeImeCompositionBounds>,
461    pub selection: Option<ChromeTextSelectionBounds>,
462}
463
464impl From<ChromeImeBoundsUpdate> for cbf::data::ime::ImeBoundsUpdate {
465    fn from(value: ChromeImeBoundsUpdate) -> Self {
466        Self {
467            composition: value.composition.map(Into::into),
468            selection: value.selection.map(Into::into),
469        }
470    }
471}
472
473impl From<cbf::data::ime::ImeBoundsUpdate> for ChromeImeBoundsUpdate {
474    fn from(value: cbf::data::ime::ImeBoundsUpdate) -> Self {
475        Self {
476            composition: value.composition.map(Into::into),
477            selection: value.selection.map(Into::into),
478        }
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::{
485        ChromeImeBoundsUpdate, ChromeImeCommitText, ChromeImeComposition,
486        ChromeImeCompositionBounds, ChromeImeRect, ChromeImeTextRange, ChromeImeTextSpan,
487        ChromeImeTextSpanStyle, ChromeImeTextSpanThickness, ChromeImeTextSpanType,
488        ChromeImeTextSpanUnderlineStyle, ChromeTextSelectionBounds,
489    };
490    use crate::data::ids::TabId;
491
492    #[test]
493    fn ime_text_span_round_trip_preserves_chrome_style() {
494        let raw = ChromeImeTextSpan {
495            r#type: ChromeImeTextSpanType::GrammarSuggestion,
496            start_offset: 2,
497            end_offset: 7,
498            chrome_style: Some(ChromeImeTextSpanStyle {
499                underline_color: 0x00112233,
500                thickness: ChromeImeTextSpanThickness::Thick,
501                underline_style: ChromeImeTextSpanUnderlineStyle::Squiggle,
502                text_color: 0x00445566,
503                background_color: 0x00778899,
504                suggestion_highlight_color: 0x00AABBCC,
505                remove_on_finish_composing: true,
506                interim_char_selection: false,
507                should_hide_suggestion_menu: true,
508            }),
509        };
510
511        let generic: cbf::data::ime::ImeTextSpan = raw.clone().into();
512        let round_trip = ChromeImeTextSpan::from(generic);
513
514        assert_eq!(round_trip, raw);
515    }
516
517    #[test]
518    fn ime_composition_and_commit_round_trip() {
519        let composition = ChromeImeComposition {
520            browsing_context_id: TabId::new(42),
521            text: "あいう".to_string(),
522            selection_start: 1,
523            selection_end: 3,
524            replacement_range: Some(ChromeImeTextRange { start: 0, end: 2 }),
525            spans: vec![ChromeImeTextSpan::new(
526                ChromeImeTextSpanType::Composition,
527                0,
528                3,
529            )],
530        };
531        let commit = ChromeImeCommitText {
532            browsing_context_id: TabId::new(42),
533            text: "確定".to_string(),
534            relative_caret_position: -1,
535            replacement_range: Some(ChromeImeTextRange { start: 0, end: 3 }),
536            spans: vec![ChromeImeTextSpan::no_decoration(
537                ChromeImeTextSpanType::Suggestion,
538                0,
539                2,
540            )],
541        };
542
543        let composition_generic: cbf::data::ime::ImeComposition = composition.clone().into();
544        let commit_generic: cbf::data::ime::ImeCommitText = commit.clone().into();
545
546        assert_eq!(ChromeImeComposition::from(composition_generic), composition);
547        assert_eq!(ChromeImeCommitText::from(commit_generic), commit);
548    }
549
550    #[test]
551    fn ime_bounds_update_round_trip() {
552        let raw = ChromeImeBoundsUpdate {
553            composition: Some(ChromeImeCompositionBounds {
554                range_start: 0,
555                range_end: 2,
556                character_bounds: vec![
557                    ChromeImeRect {
558                        x: 10,
559                        y: 20,
560                        width: 30,
561                        height: 40,
562                    },
563                    ChromeImeRect {
564                        x: 50,
565                        y: 60,
566                        width: 70,
567                        height: 80,
568                    },
569                ],
570            }),
571            selection: Some(ChromeTextSelectionBounds {
572                range_start: 1,
573                range_end: 2,
574                caret_rect: ChromeImeRect {
575                    x: 100,
576                    y: 200,
577                    width: 10,
578                    height: 20,
579                },
580                first_selection_rect: ChromeImeRect {
581                    x: 110,
582                    y: 210,
583                    width: 15,
584                    height: 25,
585                },
586            }),
587        };
588
589        let generic: cbf::data::ime::ImeBoundsUpdate = raw.clone().into();
590        let round_trip = ChromeImeBoundsUpdate::from(generic);
591
592        assert_eq!(round_trip, raw);
593    }
594}