1use std::ops::Range;
2
3use crate::core::UiRect;
4
5use super::{TextAffinity, TextCluster, TextDirection, TextHit, TextLineMetrics};
6
7#[derive(Clone, Copy, Debug, Default, PartialEq)]
8struct TextCaret {
9 index: usize,
10 affinity: TextAffinity,
11 rect: UiRect,
12}
13
14#[derive(Clone, Debug, Default, PartialEq)]
15pub struct TextLayout {
16 pub width: f32,
17 pub height: f32,
18 pub did_exceed_max_lines: bool,
19 lines: Vec<TextLineMetrics>,
20 clusters: Vec<TextCluster>,
21 carets: Vec<TextCaret>,
22}
23
24impl TextLayout {
25 pub fn new(
26 width: f32,
27 height: f32,
28 did_exceed_max_lines: bool,
29 lines: Vec<TextLineMetrics>,
30 clusters: Vec<TextCluster>,
31 carets: Vec<(usize, TextAffinity, UiRect)>,
32 ) -> Self {
33 Self {
34 width,
35 height,
36 did_exceed_max_lines,
37 lines,
38 clusters,
39 carets: carets
40 .into_iter()
41 .map(|(index, affinity, rect)| TextCaret {
42 index,
43 affinity,
44 rect,
45 })
46 .collect(),
47 }
48 }
49
50 pub fn lines(&self) -> &[TextLineMetrics] {
51 &self.lines
52 }
53
54 pub fn clusters(&self) -> &[TextCluster] {
55 &self.clusters
56 }
57
58 pub fn caret_rect(&self, index: usize) -> Option<UiRect> {
59 self.caret_rect_with_affinity(index, TextAffinity::Downstream)
60 .or_else(|| {
61 self.carets
62 .iter()
63 .find(|caret| caret.index == index)
64 .map(|c| c.rect)
65 })
66 }
67
68 pub fn caret_rect_with_affinity(&self, index: usize, affinity: TextAffinity) -> Option<UiRect> {
69 self.carets
70 .iter()
71 .find(|caret| caret.index == index && caret.affinity == affinity)
72 .map(|caret| caret.rect)
73 }
74
75 pub fn selection_rects(&self, range: Range<usize>) -> Vec<UiRect> {
76 if range.start >= range.end {
77 return Vec::new();
78 }
79 let mut rects = Vec::<UiRect>::new();
80 for cluster in self
81 .clusters
82 .iter()
83 .filter(|cluster| cluster.range.start < range.end && cluster.range.end > range.start)
84 {
85 if let Some(last) = rects.last_mut() {
86 let same_line = (last.top - cluster.bounds.top).abs() < 0.5
87 && (last.bottom - cluster.bounds.bottom).abs() < 0.5;
88 let adjacent = (last.right - cluster.bounds.left).abs() < 1.0
89 || (cluster.bounds.right - last.left).abs() < 1.0;
90 if same_line && adjacent {
91 last.left = last.left.min(cluster.bounds.left);
92 last.right = last.right.max(cluster.bounds.right);
93 continue;
94 }
95 }
96 rects.push(cluster.bounds);
97 }
98 rects
99 }
100
101 pub fn previous_cluster_boundary(&self, index: usize) -> usize {
102 self.clusters
103 .iter()
104 .flat_map(|cluster| [cluster.range.start, cluster.range.end])
105 .chain(
106 self.lines
107 .iter()
108 .flat_map(|line| [line.range.start, line.range.end]),
109 )
110 .filter(|boundary| *boundary < index)
111 .max()
112 .unwrap_or(0)
113 }
114
115 pub fn next_cluster_boundary(&self, index: usize) -> usize {
116 self.clusters
117 .iter()
118 .flat_map(|cluster| [cluster.range.start, cluster.range.end])
119 .chain(
120 self.lines
121 .iter()
122 .flat_map(|line| [line.range.start, line.range.end]),
123 )
124 .filter(|boundary| *boundary > index)
125 .min()
126 .unwrap_or(index)
127 }
128
129 pub fn hit_test(&self, x: f32, y: f32) -> TextHit {
130 let Some(line) =
131 self.lines.iter().min_by(|left, right| {
132 distance_to_axis(y, left.bounds.top, left.bounds.bottom)
133 .total_cmp(&distance_to_axis(y, right.bounds.top, right.bounds.bottom))
134 })
135 else {
136 return TextHit::default();
137 };
138 let mut clusters = self
139 .clusters
140 .iter()
141 .filter(|cluster| {
142 cluster.range.start >= line.range.start && cluster.range.end <= line.range.end
143 })
144 .peekable();
145 let Some(first) = clusters.peek().cloned() else {
146 return TextHit {
147 index: line.range.start,
148 affinity: TextAffinity::Downstream,
149 inside: rect_contains(line.bounds, x, y),
150 };
151 };
152 let mut nearest = first;
153 let mut nearest_distance = distance_to_axis(x, first.bounds.left, first.bounds.right);
154 for cluster in clusters {
155 let distance = distance_to_axis(x, cluster.bounds.left, cluster.bounds.right);
156 if distance < nearest_distance {
157 nearest = cluster;
158 nearest_distance = distance;
159 }
160 }
161 let midpoint = (nearest.bounds.left + nearest.bounds.right) * 0.5;
162 let leading_half = x < midpoint;
163 let index = match nearest.direction {
164 TextDirection::RightToLeft if leading_half => nearest.range.end,
165 TextDirection::RightToLeft => nearest.range.start,
166 _ if leading_half => nearest.range.start,
167 _ => nearest.range.end,
168 };
169 TextHit {
170 index,
171 affinity: if leading_half {
172 TextAffinity::Downstream
173 } else {
174 TextAffinity::Upstream
175 },
176 inside: rect_contains(line.bounds, x, y),
177 }
178 }
179}
180
181fn distance_to_axis(value: f32, start: f32, end: f32) -> f32 {
182 if value < start {
183 start - value
184 } else if value > end {
185 value - end
186 } else {
187 0.0
188 }
189}
190
191fn rect_contains(rect: UiRect, x: f32, y: f32) -> bool {
192 x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
193}