Skip to main content

lgui_core/text/
layout.rs

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}