use super::{TextInputMethodError, TextSelection};
use crate::{Matrix, Point, Rect};
use std::fmt;
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextInputMethodTextBounds {
pub rectangle: Rect,
pub transform: Matrix,
}
impl TextInputMethodTextBounds {
pub const fn new(rectangle: Rect) -> Self {
Self::with_transform(rectangle, Matrix::IDENTITY)
}
pub const fn with_transform(rectangle: Rect, transform: Matrix) -> Self {
Self {
rectangle,
transform,
}
}
pub fn corners(&self) -> [Point; 4] {
let rect = self.rectangle;
[
Point::new(rect.x, rect.y),
Point::new(rect.right(), rect.y),
Point::new(rect.right(), rect.bottom()),
Point::new(rect.x, rect.bottom()),
]
.map(|point| point.transform(self.transform))
}
fn is_valid(&self) -> bool {
let rect = self.rectangle;
[rect.x, rect.y, rect.width, rect.height]
.into_iter()
.all(f64::is_finite)
&& rect.width >= 0.0
&& rect.height >= 0.0
&& self
.corners()
.into_iter()
.all(|point| point.x.is_finite() && point.y.is_finite())
}
}
#[derive(Clone)]
pub struct TextInputMethodRangeGeometry {
range: TextSelection,
fragments: Rc<[TextInputMethodTextBounds]>,
visual_version: u64,
is_current: Rc<dyn Fn() -> bool>,
}
impl TextInputMethodRangeGeometry {
pub fn new(
range: TextSelection,
fragments: impl Into<Rc<[TextInputMethodTextBounds]>>,
is_current: impl Fn() -> bool + 'static,
) -> Result<Self, TextInputMethodError> {
if range.start < 0 || range.end < range.start {
return Err(TextInputMethodError::InvalidRange);
}
let fragments = fragments.into();
if fragments.is_empty() || fragments.iter().any(|fragment| !fragment.is_valid()) {
return Err(TextInputMethodError::Unavailable);
}
Ok(Self {
range,
fragments,
visual_version: crate::visual::geometry_version(),
is_current: Rc::new(is_current),
})
}
pub fn range(&self) -> TextSelection {
self.range
}
pub fn fragments(&self) -> &[TextInputMethodTextBounds] {
&self.fragments
}
pub fn is_current(&self) -> bool {
self.visual_version == crate::visual::geometry_version()
&& (self.is_current)()
&& self.visual_version == crate::visual::geometry_version()
}
}
impl fmt::Debug for TextInputMethodRangeGeometry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TextInputMethodRangeGeometry")
.field("range", &self.range)
.field("fragments", &self.fragments)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Visual;
use std::cell::Cell;
#[test]
fn input_range_geometry_keeps_utf16_indices_and_unflattened_fragments() {
let fragment = TextInputMethodTextBounds::with_transform(
Rect::new(2.0, 3.0, 4.0, 5.0),
Matrix::new(0.0, 1.0, -1.0, 0.0, 10.0, 20.0),
);
let result =
TextInputMethodRangeGeometry::new(TextSelection::new(2, 3), vec![fragment], || true)
.unwrap();
assert_eq!(result.range(), TextSelection::new(2, 3));
assert_eq!(result.fragments(), &[fragment]);
assert_eq!(
fragment.corners(),
[
Point::new(7.0, 22.0),
Point::new(7.0, 26.0),
Point::new(2.0, 26.0),
Point::new(2.0, 22.0),
]
);
assert!(result.is_current());
}
#[test]
fn input_range_geometry_rejects_missing_invalid_and_nonfinite_bounds() {
let good = TextInputMethodTextBounds::new(Rect::new(0.0, 0.0, 1.0, 10.0));
for range in [TextSelection::new(-1, 2), TextSelection::new(3, 2)] {
assert_eq!(
TextInputMethodRangeGeometry::new(range, vec![good], || true).unwrap_err(),
TextInputMethodError::InvalidRange
);
}
for fragments in [
vec![],
vec![TextInputMethodTextBounds::new(Rect::new(
0.0, 0.0, -1.0, 10.0,
))],
vec![TextInputMethodTextBounds::new(Rect::new(
f64::NAN,
0.0,
1.0,
10.0,
))],
vec![TextInputMethodTextBounds::with_transform(
good.rectangle,
Matrix::new(f64::INFINITY, 0.0, 0.0, 1.0, 0.0, 0.0),
)],
] {
assert_eq!(
TextInputMethodRangeGeometry::new(TextSelection::new(0, 0), fragments, || true)
.unwrap_err(),
TextInputMethodError::Unavailable
);
}
let empty = TextInputMethodRangeGeometry::new(
TextSelection::new(0, 0),
vec![TextInputMethodTextBounds::new(Rect::new(
3.0, 4.0, 0.0, 8.0,
))],
|| true,
)
.unwrap();
assert_eq!(empty.fragments()[0].rectangle.height, 8.0);
assert_eq!(empty.fragments()[0].rectangle.width, 0.0);
}
#[test]
fn input_range_geometry_rejects_client_changes_and_reentrant_visual_changes() {
let _scope = crate::threading::Dispatcher::unit_test_scope();
let current = Rc::new(Cell::new(true));
let captured = current.clone();
let fragment = TextInputMethodTextBounds::new(Rect::new(0.0, 0.0, 1.0, 10.0));
let result = TextInputMethodRangeGeometry::new(
TextSelection::new(0, 0),
vec![fragment],
move || captured.get(),
)
.unwrap();
assert!(result.is_current());
current.set(false);
assert!(!result.is_current());
let visual = Visual::new();
let result = TextInputMethodRangeGeometry::new(
TextSelection::new(0, 0),
vec![fragment],
move || {
visual.set_bounds(Rect::new(0.0, 0.0, 100.0, 20.0));
true
},
)
.unwrap();
assert!(!result.is_current());
assert!(!result.is_current());
}
}