ferroui_base/input/text_input/
text_input_method_range_geometry.rs1use super::{TextInputMethodError, TextSelection};
4use crate::{Matrix, Point, Rect};
5use std::fmt;
6use std::rc::Rc;
7
8#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct TextInputMethodTextBounds {
15 pub rectangle: Rect,
16 pub transform: Matrix,
17}
18
19impl TextInputMethodTextBounds {
20 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 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#[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 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 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}