Skip to main content

ferroui_base/input/text_input/
text_input_method_range_geometry.rs

1//! A short-lived geometry snapshot for a platform query of the visible text.
2
3use super::{TextInputMethodError, TextSelection};
4use crate::{Matrix, Point, Rect};
5use std::fmt;
6use std::rc::Rc;
7
8/// One shaped text fragment and its transform to the semantic input owner.
9///
10/// Keeping the local rectangle separate from its transform lets a platform
11/// transform its four corners before taking screen bounds. Taking a bounding
12/// box at each intermediate visual would inflate rotated text unnecessarily.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct TextInputMethodTextBounds {
15    pub rectangle: Rect,
16    pub transform: Matrix,
17}
18
19impl TextInputMethodTextBounds {
20    /// A rectangle already expressed in the semantic owner's coordinates.
21    pub const fn new(rectangle: Rect) -> Self {
22        Self::with_transform(rectangle, Matrix::IDENTITY)
23    }
24
25    pub const fn with_transform(rectangle: Rect, transform: Matrix) -> Self {
26        Self {
27            rectangle,
28            transform,
29        }
30    }
31
32    /// The actual fragment corners, in the semantic owner's coordinates.
33    pub fn corners(&self) -> [Point; 4] {
34        let rect = self.rectangle;
35        [
36            Point::new(rect.x, rect.y),
37            Point::new(rect.right(), rect.y),
38            Point::new(rect.right(), rect.bottom()),
39            Point::new(rect.x, rect.bottom()),
40        ]
41        .map(|point| point.transform(self.transform))
42    }
43
44    fn is_valid(&self) -> bool {
45        let rect = self.rectangle;
46        [rect.x, rect.y, rect.width, rect.height]
47            .into_iter()
48            .all(f64::is_finite)
49            && rect.width >= 0.0
50            && rect.height >= 0.0
51            && self
52                .corners()
53                .into_iter()
54                .all(|point| point.x.is_finite() && point.y.is_finite())
55    }
56}
57
58/// Complete geometry for one ordered UTF-16 range of the currently displayed
59/// input story, including any visible composition preview.
60///
61/// The fragments are immutable; an empty range describes the requested
62/// insertion position. A nonempty range can span shaped clusters, bidi runs,
63/// lines or linked views. No fragment is a candidate-window rectangle.
64///
65/// Layout and native property getters can invoke application code. Adapters
66/// must call [`is_current`](Self::is_current) after callbacks and before each
67/// native publication, in addition to checking their input-session ownership.
68/// A snapshot never authorizes an edit or keeps an input connection current.
69#[derive(Clone)]
70pub struct TextInputMethodRangeGeometry {
71    range: TextSelection,
72    fragments: Rc<[TextInputMethodTextBounds]>,
73    visual_version: u64,
74    is_current: Rc<dyn Fn() -> bool>,
75}
76
77impl TextInputMethodRangeGeometry {
78    /// Creates a snapshot with a client-supplied validity check. The check must
79    /// be observational and detect document, layout, scrolling and view changes
80    /// not already represented by the framework's visual geometry version.
81    /// Capture weak references so a retained result cannot retain its control.
82    ///
83    /// Empty fragments mean geometry is unavailable, not a request to use the
84    /// current caret. Zero-width fragments are valid for line/paragraph breaks.
85    pub fn new(
86        range: TextSelection,
87        fragments: impl Into<Rc<[TextInputMethodTextBounds]>>,
88        is_current: impl Fn() -> bool + 'static,
89    ) -> Result<Self, TextInputMethodError> {
90        if range.start < 0 || range.end < range.start {
91            return Err(TextInputMethodError::InvalidRange);
92        }
93        let fragments = fragments.into();
94        if fragments.is_empty() || fragments.iter().any(|fragment| !fragment.is_valid()) {
95            return Err(TextInputMethodError::Unavailable);
96        }
97        Ok(Self {
98            range,
99            fragments,
100            visual_version: crate::visual::geometry_version(),
101            is_current: Rc::new(is_current),
102        })
103    }
104
105    pub fn range(&self) -> TextSelection {
106        self.range
107    }
108
109    pub fn fragments(&self) -> &[TextInputMethodTextBounds] {
110        &self.fragments
111    }
112
113    /// Checks both client layout state and synchronous visual changes. The
114    /// second version check also rejects a visual change made by the callback.
115    /// The caller must still recheck its own generation after this client call.
116    pub fn is_current(&self) -> bool {
117        self.visual_version == crate::visual::geometry_version()
118            && (self.is_current)()
119            && self.visual_version == crate::visual::geometry_version()
120    }
121}
122
123impl fmt::Debug for TextInputMethodRangeGeometry {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        f.debug_struct("TextInputMethodRangeGeometry")
126            .field("range", &self.range)
127            .field("fragments", &self.fragments)
128            .finish_non_exhaustive()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::Visual;
136    use std::cell::Cell;
137
138    #[test]
139    fn input_range_geometry_keeps_utf16_indices_and_unflattened_fragments() {
140        let fragment = TextInputMethodTextBounds::with_transform(
141            Rect::new(2.0, 3.0, 4.0, 5.0),
142            Matrix::new(0.0, 1.0, -1.0, 0.0, 10.0, 20.0),
143        );
144        let result =
145            TextInputMethodRangeGeometry::new(TextSelection::new(2, 3), vec![fragment], || true)
146                .unwrap();
147        assert_eq!(result.range(), TextSelection::new(2, 3));
148        assert_eq!(result.fragments(), &[fragment]);
149        assert_eq!(
150            fragment.corners(),
151            [
152                Point::new(7.0, 22.0),
153                Point::new(7.0, 26.0),
154                Point::new(2.0, 26.0),
155                Point::new(2.0, 22.0),
156            ]
157        );
158        assert!(result.is_current());
159    }
160
161    #[test]
162    fn input_range_geometry_rejects_missing_invalid_and_nonfinite_bounds() {
163        let good = TextInputMethodTextBounds::new(Rect::new(0.0, 0.0, 1.0, 10.0));
164        for range in [TextSelection::new(-1, 2), TextSelection::new(3, 2)] {
165            assert_eq!(
166                TextInputMethodRangeGeometry::new(range, vec![good], || true).unwrap_err(),
167                TextInputMethodError::InvalidRange
168            );
169        }
170        for fragments in [
171            vec![],
172            vec![TextInputMethodTextBounds::new(Rect::new(
173                0.0, 0.0, -1.0, 10.0,
174            ))],
175            vec![TextInputMethodTextBounds::new(Rect::new(
176                f64::NAN,
177                0.0,
178                1.0,
179                10.0,
180            ))],
181            vec![TextInputMethodTextBounds::with_transform(
182                good.rectangle,
183                Matrix::new(f64::INFINITY, 0.0, 0.0, 1.0, 0.0, 0.0),
184            )],
185        ] {
186            assert_eq!(
187                TextInputMethodRangeGeometry::new(TextSelection::new(0, 0), fragments, || true)
188                    .unwrap_err(),
189                TextInputMethodError::Unavailable
190            );
191        }
192        let empty = TextInputMethodRangeGeometry::new(
193            TextSelection::new(0, 0),
194            vec![TextInputMethodTextBounds::new(Rect::new(
195                3.0, 4.0, 0.0, 8.0,
196            ))],
197            || true,
198        )
199        .unwrap();
200        assert_eq!(empty.fragments()[0].rectangle.height, 8.0);
201        assert_eq!(empty.fragments()[0].rectangle.width, 0.0);
202    }
203
204    #[test]
205    fn input_range_geometry_rejects_client_changes_and_reentrant_visual_changes() {
206        let _scope = crate::threading::Dispatcher::unit_test_scope();
207        let current = Rc::new(Cell::new(true));
208        let captured = current.clone();
209        let fragment = TextInputMethodTextBounds::new(Rect::new(0.0, 0.0, 1.0, 10.0));
210        let result = TextInputMethodRangeGeometry::new(
211            TextSelection::new(0, 0),
212            vec![fragment],
213            move || captured.get(),
214        )
215        .unwrap();
216        assert!(result.is_current());
217        current.set(false);
218        assert!(!result.is_current());
219
220        let visual = Visual::new();
221        let result = TextInputMethodRangeGeometry::new(
222            TextSelection::new(0, 0),
223            vec![fragment],
224            move || {
225                visual.set_bounds(Rect::new(0.0, 0.0, 100.0, 20.0));
226                true
227            },
228        )
229        .unwrap();
230        assert!(!result.is_current());
231        assert!(!result.is_current());
232    }
233}