1use std::{ops::Range, sync::LazyLock};
2
3use crate::text::AttrsList;
4use cosmic_text::{
5 Affinity, Buffer, BufferLine, Cursor, FontSystem, LayoutCursor, LayoutGlyph, LineEnding,
6 LineIter, Metrics, Scroll, Shaping, Wrap,
7};
8use parking_lot::Mutex;
9use peniko::kurbo::{Point, Size};
10
11pub static FONT_SYSTEM: LazyLock<Mutex<FontSystem>> = LazyLock::new(|| {
12 let mut font_system = FontSystem::new();
13 #[cfg(target_os = "macos")]
14 font_system.db_mut().set_sans_serif_family("Helvetica Neue");
15 #[cfg(target_os = "windows")]
16 font_system.db_mut().set_sans_serif_family("Segoe UI");
17 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
18 font_system.db_mut().set_sans_serif_family("Noto Sans");
19 Mutex::new(font_system)
20});
21
22#[derive(Debug)]
24pub struct LayoutRun<'a> {
25 pub line_i: usize,
27 pub text: &'a str,
29 pub rtl: bool,
31 pub glyphs: &'a [LayoutGlyph],
33 pub max_ascent: f32,
35 pub max_descent: f32,
37 pub line_y: f32,
39 pub line_top: f32,
41 pub line_height: f32,
43 pub line_w: f32,
45}
46
47impl LayoutRun<'_> {
48 pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> {
53 let mut x_start = None;
54 let mut x_end = None;
55 let rtl_factor = if self.rtl { 1. } else { 0. };
56 let ltr_factor = 1. - rtl_factor;
57 for glyph in self.glyphs.iter() {
58 let cursor = self.cursor_from_glyph_left(glyph);
59 if cursor >= cursor_start && cursor <= cursor_end {
60 if x_start.is_none() {
61 x_start = Some(glyph.x + glyph.w * rtl_factor);
62 }
63 x_end = Some(glyph.x + glyph.w * rtl_factor);
64 }
65 let cursor = self.cursor_from_glyph_right(glyph);
66 if cursor >= cursor_start && cursor <= cursor_end {
67 if x_start.is_none() {
68 x_start = Some(glyph.x + glyph.w * ltr_factor);
69 }
70 x_end = Some(glyph.x + glyph.w * ltr_factor);
71 }
72 }
73 if let Some(x_start) = x_start {
74 let x_end = x_end.expect("end of cursor not found");
75 let (x_start, x_end) = if x_start < x_end {
76 (x_start, x_end)
77 } else {
78 (x_end, x_start)
79 };
80 Some((x_start, x_end - x_start))
81 } else {
82 None
83 }
84 }
85
86 fn cursor_from_glyph_left(&self, glyph: &LayoutGlyph) -> Cursor {
87 if self.rtl {
88 Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
89 } else {
90 Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
91 }
92 }
93
94 pub fn cursor_from_glyph_right(&self, glyph: &LayoutGlyph) -> Cursor {
95 if self.rtl {
96 Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
97 } else {
98 Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
99 }
100 }
101}
102
103#[derive(Debug)]
105pub struct LayoutRunIter<'b> {
106 text_layout: &'b TextLayout,
107 line_i: usize,
108 layout_i: usize,
109 total_height: f32,
110 line_top: f32,
111}
112
113impl<'b> LayoutRunIter<'b> {
114 pub fn new(text_layout: &'b TextLayout) -> Self {
115 Self {
116 text_layout,
117 line_i: text_layout.buffer.scroll().line,
118 layout_i: 0,
119 total_height: 0.0,
120 line_top: 0.0,
121 }
122 }
123}
124
125impl<'b> Iterator for LayoutRunIter<'b> {
126 type Item = LayoutRun<'b>;
127
128 fn next(&mut self) -> Option<Self::Item> {
129 while let Some(line) = self.text_layout.buffer.lines.get(self.line_i) {
130 let shape = line.shape_opt().as_ref()?;
131 let layout = line.layout_opt().as_ref()?;
132 while let Some(layout_line) = layout.get(self.layout_i) {
133 self.layout_i += 1;
134
135 let line_height = layout_line
136 .line_height_opt
137 .unwrap_or(self.text_layout.buffer.metrics().line_height);
138 self.total_height += line_height;
139
140 let line_top = self.line_top - self.text_layout.buffer.scroll().vertical;
141 let glyph_height = layout_line.max_ascent + layout_line.max_descent;
142 let centering_offset = (line_height - glyph_height) / 2.0;
143 let line_y = line_top + centering_offset + layout_line.max_ascent;
144 if let Some(height) = self.text_layout.height_opt {
145 if line_y > height {
146 return None;
147 }
148 }
149 self.line_top += line_height;
150 if line_y < 0.0 {
151 continue;
152 }
153
154 return Some(LayoutRun {
155 line_i: self.line_i,
156 text: line.text(),
157 rtl: shape.rtl,
158 glyphs: &layout_line.glyphs,
159 max_ascent: layout_line.max_ascent,
160 max_descent: layout_line.max_descent,
161 line_y,
162 line_top,
163 line_height,
164 line_w: layout_line.w,
165 });
166 }
167 self.line_i += 1;
168 self.layout_i = 0;
169 }
170
171 None
172 }
173}
174
175pub struct HitPosition {
176 pub line: usize,
178 pub point: Point,
180 pub glyph_ascent: f64,
182 pub glyph_descent: f64,
184}
185
186pub struct HitPoint {
187 pub line: usize,
189 pub index: usize,
191 pub is_inside: bool,
198}
199
200#[derive(Clone, Debug)]
201pub struct TextLayout {
202 buffer: Buffer,
203 lines_range: Vec<Range<usize>>,
204 width_opt: Option<f32>,
205 height_opt: Option<f32>,
206}
207
208impl Default for TextLayout {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214impl TextLayout {
215 pub fn new() -> Self {
216 TextLayout {
217 buffer: Buffer::new_empty(Metrics::new(16.0, 16.0)),
218 lines_range: Vec::new(),
219 width_opt: None,
220 height_opt: None,
221 }
222 }
223
224 pub fn new_with_text(text: &str, attrs_list: AttrsList) -> Self {
225 let mut layout = Self::new();
226 layout.set_text(text, attrs_list);
227 layout
228 }
229
230 pub fn set_text(&mut self, text: &str, attrs_list: AttrsList) {
231 self.buffer.lines.clear();
232 self.lines_range.clear();
233 let mut attrs_list = attrs_list.0;
234 for (range, ending) in LineIter::new(text) {
235 self.lines_range.push(range.clone());
236 let line_text = &text[range];
237 let new_attrs = attrs_list
238 .clone()
239 .split_off(line_text.len() + ending.as_str().len());
240 self.buffer.lines.push(BufferLine::new(
241 line_text,
242 ending,
243 attrs_list.clone(),
244 Shaping::Advanced,
245 ));
246 attrs_list = new_attrs;
247 }
248 if self.buffer.lines.is_empty() {
249 self.buffer.lines.push(BufferLine::new(
250 "",
251 LineEnding::default(),
252 attrs_list,
253 Shaping::Advanced,
254 ));
255 self.lines_range.push(0..0)
256 }
257 self.buffer.set_scroll(Scroll::default());
258 let mut font_system = FONT_SYSTEM.lock();
259 self.buffer.shape_until_scroll(&mut font_system, false);
260 }
261
262 pub fn set_wrap(&mut self, wrap: Wrap) {
263 let mut font_system = FONT_SYSTEM.lock();
264 self.buffer.set_wrap(&mut font_system, wrap);
265 }
266
267 pub fn set_tab_width(&mut self, tab_width: usize) {
268 let mut font_system = FONT_SYSTEM.lock();
269 self.buffer
270 .set_tab_width(&mut font_system, tab_width as u16);
271 }
272
273 pub fn set_size(&mut self, width: f32, height: f32) {
274 let mut font_system = FONT_SYSTEM.lock();
275 self.width_opt = Some(width);
276 self.height_opt = Some(height);
277 self.buffer
278 .set_size(&mut font_system, Some(width), Some(height));
279 }
280
281 pub fn metrics(&self) -> Metrics {
282 self.buffer.metrics()
283 }
284
285 pub fn lines(&self) -> &[BufferLine] {
286 &self.buffer.lines
287 }
288
289 pub fn lines_range(&self) -> &[Range<usize>] {
290 &self.lines_range
291 }
292
293 pub fn layout_runs(&self) -> LayoutRunIter {
294 LayoutRunIter::new(self)
295 }
296
297 pub fn layout_cursor(&mut self, cursor: Cursor) -> LayoutCursor {
298 let line = cursor.line;
299 let mut font_system = FONT_SYSTEM.lock();
300 self.buffer
301 .layout_cursor(&mut font_system, cursor)
302 .unwrap_or_else(|| LayoutCursor::new(line, 0, 0))
303 }
304
305 pub fn hit_position(&self, idx: usize) -> HitPosition {
306 let mut last_line = 0;
307 let mut last_end: usize = 0;
308 let mut offset = 0;
309 let mut last_glyph_width = 0.0;
310 let mut last_position = HitPosition {
311 line: 0,
312 point: Point::ZERO,
313 glyph_ascent: 0.0,
314 glyph_descent: 0.0,
315 };
316 for (line, run) in self.layout_runs().enumerate() {
317 if run.line_i > last_line {
318 last_line = run.line_i;
319 offset += last_end + 1;
320 }
321 for glyph in run.glyphs {
322 if glyph.start + offset > idx {
323 last_position.point.x += last_glyph_width as f64;
324 return last_position;
325 }
326 last_end = glyph.end;
327 last_glyph_width = glyph.w;
328 last_position = HitPosition {
329 line,
330 point: Point::new(glyph.x as f64, run.line_y as f64),
331 glyph_ascent: run.max_ascent as f64,
332 glyph_descent: run.max_descent as f64,
333 };
334 if (glyph.start + offset..glyph.end + offset).contains(&idx) {
335 return last_position;
336 }
337 }
338 }
339
340 if idx > 0 {
341 last_position.point.x += last_glyph_width as f64;
342 return last_position;
343 }
344
345 HitPosition {
346 line: 0,
347 point: Point::ZERO,
348 glyph_ascent: 0.0,
349 glyph_descent: 0.0,
350 }
351 }
352
353 pub fn hit_point(&self, point: Point) -> HitPoint {
354 if let Some(cursor) = self.hit(point.x as f32, point.y as f32) {
355 let size = self.size();
356 let is_inside = point.x <= size.width && point.y <= size.height;
357 HitPoint {
358 line: cursor.line,
359 index: cursor.index,
360 is_inside,
361 }
362 } else {
363 HitPoint {
364 line: 0,
365 index: 0,
366 is_inside: false,
367 }
368 }
369 }
370
371 pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
373 self.buffer.hit(x, y)
374 }
375
376 pub fn line_col_position(&self, line: usize, col: usize) -> HitPosition {
377 let mut last_glyph: Option<&LayoutGlyph> = None;
378 let mut last_line = 0;
379 let mut last_line_y = 0.0;
380 let mut last_glyph_ascent = 0.0;
381 let mut last_glyph_descent = 0.0;
382 for (current_line, run) in self.layout_runs().enumerate() {
383 for glyph in run.glyphs {
384 match run.line_i.cmp(&line) {
385 std::cmp::Ordering::Equal => {
386 if glyph.start > col {
387 return HitPosition {
388 line: last_line,
389 point: Point::new(
390 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
391 last_line_y as f64,
392 ),
393 glyph_ascent: last_glyph_ascent as f64,
394 glyph_descent: last_glyph_descent as f64,
395 };
396 }
397 if (glyph.start..glyph.end).contains(&col) {
398 return HitPosition {
399 line: current_line,
400 point: Point::new(glyph.x as f64, run.line_y as f64),
401 glyph_ascent: run.max_ascent as f64,
402 glyph_descent: run.max_descent as f64,
403 };
404 }
405 }
406 std::cmp::Ordering::Greater => {
407 return HitPosition {
408 line: last_line,
409 point: Point::new(
410 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
411 last_line_y as f64,
412 ),
413 glyph_ascent: last_glyph_ascent as f64,
414 glyph_descent: last_glyph_descent as f64,
415 };
416 }
417 std::cmp::Ordering::Less => {}
418 };
419 last_glyph = Some(glyph);
420 }
421 last_line = current_line;
422 last_line_y = run.line_y;
423 last_glyph_ascent = run.max_ascent;
424 last_glyph_descent = run.max_descent;
425 }
426
427 HitPosition {
428 line: last_line,
429 point: Point::new(
430 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
431 last_line_y as f64,
432 ),
433 glyph_ascent: last_glyph_ascent as f64,
434 glyph_descent: last_glyph_descent as f64,
435 }
436 }
437
438 pub fn size(&self) -> Size {
439 self.buffer
440 .layout_runs()
441 .fold(Size::new(0.0, 0.0), |mut size, run| {
442 let new_width = run.line_w as f64;
443 if new_width > size.width {
444 size.width = new_width;
445 }
446
447 size.height += run.line_height as f64;
448
449 size
450 })
451 }
452}