1use std::{ops::Range, sync::LazyLock};
2
3use crate::text::AttrsList;
4use cosmic_text::{
5 Affinity, Align, Buffer, BufferLine, Cursor, FontSystem, LayoutCursor, LayoutGlyph, LineEnding,
6 LineIter, Metrics, Scroll, Shaping, Wrap,
7};
8use parking_lot::Mutex;
9use peniko::kurbo::{Point, Size};
10use unicode_segmentation::UnicodeSegmentation;
11
12pub static FONT_SYSTEM: LazyLock<Mutex<FontSystem>> = LazyLock::new(|| {
13 let mut font_system = FontSystem::new();
14 #[cfg(target_os = "macos")]
15 font_system.db_mut().set_sans_serif_family("Helvetica Neue");
16 #[cfg(target_os = "windows")]
17 font_system.db_mut().set_sans_serif_family("Segoe UI");
18 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
19 font_system.db_mut().set_sans_serif_family("Noto Sans");
20 Mutex::new(font_system)
21});
22
23#[derive(Debug)]
25pub struct LayoutRun<'a> {
26 pub line_i: usize,
28 pub text: &'a str,
30 pub rtl: bool,
32 pub glyphs: &'a [LayoutGlyph],
34 pub max_ascent: f32,
36 pub max_descent: f32,
38 pub line_y: f32,
40 pub line_top: f32,
42 pub line_height: f32,
44 pub line_w: f32,
46}
47
48impl LayoutRun<'_> {
49 pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> {
54 let mut x_start = None;
55 let mut x_end = None;
56 let rtl_factor = if self.rtl { 1. } else { 0. };
57 let ltr_factor = 1. - rtl_factor;
58 for glyph in self.glyphs.iter() {
59 let cursor = self.cursor_from_glyph_left(glyph);
60 if cursor >= cursor_start && cursor <= cursor_end {
61 if x_start.is_none() {
62 x_start = Some(glyph.x + glyph.w * rtl_factor);
63 }
64 x_end = Some(glyph.x + glyph.w * rtl_factor);
65 }
66 let cursor = self.cursor_from_glyph_right(glyph);
67 if cursor >= cursor_start && cursor <= cursor_end {
68 if x_start.is_none() {
69 x_start = Some(glyph.x + glyph.w * ltr_factor);
70 }
71 x_end = Some(glyph.x + glyph.w * ltr_factor);
72 }
73 }
74 if let Some(x_start) = x_start {
75 let x_end = x_end.expect("end of cursor not found");
76 let (x_start, x_end) = if x_start < x_end {
77 (x_start, x_end)
78 } else {
79 (x_end, x_start)
80 };
81 Some((x_start, x_end - x_start))
82 } else {
83 None
84 }
85 }
86
87 fn cursor_from_glyph_left(&self, glyph: &LayoutGlyph) -> Cursor {
88 if self.rtl {
89 Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
90 } else {
91 Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
92 }
93 }
94
95 pub fn cursor_from_glyph_right(&self, glyph: &LayoutGlyph) -> Cursor {
96 if self.rtl {
97 Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
98 } else {
99 Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
100 }
101 }
102}
103
104#[derive(Debug)]
106pub struct LayoutRunIter<'b> {
107 text_layout: &'b TextLayout,
108 line_i: usize,
109 layout_i: usize,
110 total_height: f32,
111 line_top: f32,
112}
113
114impl<'b> LayoutRunIter<'b> {
115 pub fn new(text_layout: &'b TextLayout) -> Self {
116 Self {
117 text_layout,
118 line_i: text_layout.buffer.scroll().line,
119 layout_i: 0,
120 total_height: 0.0,
121 line_top: 0.0,
122 }
123 }
124}
125
126impl<'b> Iterator for LayoutRunIter<'b> {
127 type Item = LayoutRun<'b>;
128
129 fn next(&mut self) -> Option<Self::Item> {
130 while let Some(line) = self.text_layout.buffer.lines.get(self.line_i) {
131 let shape = line.shape_opt()?;
132 let layout = line.layout_opt()?;
133 while let Some(layout_line) = layout.get(self.layout_i) {
134 self.layout_i += 1;
135
136 let line_height = layout_line
137 .line_height_opt
138 .unwrap_or(self.text_layout.buffer.metrics().line_height);
139 self.total_height += line_height;
140
141 let line_top = self.line_top - self.text_layout.buffer.scroll().vertical;
142 let glyph_height = layout_line.max_ascent + layout_line.max_descent;
143 let centering_offset = (line_height - glyph_height) / 2.0;
144 let line_y = line_top + centering_offset + layout_line.max_ascent;
145 if let Some(height) = self.text_layout.height_opt {
146 if line_y > height {
147 return None;
148 }
149 }
150 self.line_top += line_height;
151 if line_y < 0.0 {
152 continue;
153 }
154
155 return Some(LayoutRun {
156 line_i: self.line_i,
157 text: line.text(),
158 rtl: shape.rtl,
159 glyphs: &layout_line.glyphs,
160 max_ascent: layout_line.max_ascent,
161 max_descent: layout_line.max_descent,
162 line_y,
163 line_top,
164 line_height,
165 line_w: layout_line.w,
166 });
167 }
168 self.line_i += 1;
169 self.layout_i = 0;
170 }
171
172 None
173 }
174}
175
176pub struct HitPosition {
177 pub line: usize,
179 pub point: Point,
181 pub glyph_ascent: f64,
183 pub glyph_descent: f64,
185}
186
187pub struct HitPoint {
188 pub line: usize,
190 pub index: usize,
192 pub is_inside: bool,
199 pub affinity: Affinity,
200}
201
202#[derive(Clone, Debug)]
203pub struct TextLayout {
204 buffer: Buffer,
205 lines_range: Vec<Range<usize>>,
206 width_opt: Option<f32>,
207 height_opt: Option<f32>,
208}
209
210impl Default for TextLayout {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216impl TextLayout {
217 pub fn new() -> Self {
218 TextLayout {
219 buffer: Buffer::new_empty(Metrics::new(16.0, 16.0)),
220 lines_range: Vec::new(),
221 width_opt: None,
222 height_opt: None,
223 }
224 }
225
226 pub fn new_with_text(text: &str, attrs_list: AttrsList, align: Option<Align>) -> Self {
227 let mut layout = Self::new();
228 layout.set_text(text, attrs_list, align);
229 layout
230 }
231
232 pub fn set_text(&mut self, text: &str, attrs_list: AttrsList, align: Option<Align>) {
233 self.buffer.lines.clear();
234 self.lines_range.clear();
235 let mut attrs_list = attrs_list.0;
236 for (range, ending) in LineIter::new(text) {
237 self.lines_range.push(range.clone());
238 let line_text = &text[range];
239 let new_attrs = attrs_list
240 .clone()
241 .split_off(line_text.len() + ending.as_str().len());
242 let mut line =
243 BufferLine::new(line_text, ending, attrs_list.clone(), Shaping::Advanced);
244 line.set_align(align);
245 self.buffer.lines.push(line);
246 attrs_list = new_attrs;
247 }
248 if self.buffer.lines.is_empty() {
249 let mut line =
250 BufferLine::new("", LineEnding::default(), attrs_list, Shaping::Advanced);
251 line.set_align(align);
252 self.buffer.lines.push(line);
253 self.lines_range.push(0..0)
254 }
255 self.buffer.set_scroll(Scroll::default());
256
257 let mut font_system = FONT_SYSTEM.lock();
258
259 let needs_two_pass =
261 align.is_some() && align != Some(Align::Left) && self.width_opt.is_none();
262 if needs_two_pass {
263 self.buffer.shape_until_scroll(&mut font_system, false);
265
266 let measured_width = self
268 .buffer
269 .layout_runs()
270 .fold(0.0f32, |width, run| width.max(run.line_w));
271
272 if measured_width > 0.0 {
274 self.buffer
275 .set_size(&mut font_system, Some(measured_width), self.height_opt);
276 self.buffer.shape_until_scroll(&mut font_system, false);
278 }
279 } else {
280 self.buffer.shape_until_scroll(&mut font_system, false);
282 }
283 }
284
285 pub fn set_wrap(&mut self, wrap: Wrap) {
286 let mut font_system = FONT_SYSTEM.lock();
287 self.buffer.set_wrap(&mut font_system, wrap);
288 }
289
290 pub fn set_tab_width(&mut self, tab_width: usize) {
291 let mut font_system = FONT_SYSTEM.lock();
292 self.buffer
293 .set_tab_width(&mut font_system, tab_width as u16);
294 }
295
296 pub fn set_size(&mut self, width: f32, height: f32) {
297 let mut font_system = FONT_SYSTEM.lock();
298 self.width_opt = Some(width);
299 self.height_opt = Some(height);
300 self.buffer
301 .set_size(&mut font_system, Some(width), Some(height));
302 }
303
304 pub fn metrics(&self) -> Metrics {
305 self.buffer.metrics()
306 }
307
308 pub fn lines(&self) -> &[BufferLine] {
309 &self.buffer.lines
310 }
311
312 pub fn lines_range(&self) -> &[Range<usize>] {
313 &self.lines_range
314 }
315
316 pub fn layout_runs(&self) -> LayoutRunIter<'_> {
317 LayoutRunIter::new(self)
318 }
319
320 pub fn layout_cursor(&mut self, cursor: Cursor) -> LayoutCursor {
321 let line = cursor.line;
322 let mut font_system = FONT_SYSTEM.lock();
323 self.buffer
324 .layout_cursor(&mut font_system, cursor)
325 .unwrap_or_else(|| LayoutCursor::new(line, 0, 0))
326 }
327
328 pub fn hit_position(&self, idx: usize) -> HitPosition {
329 self.hit_position_aff(idx, Affinity::Before)
330 }
331
332 pub fn hit_position_aff(&self, idx: usize, affinity: Affinity) -> HitPosition {
333 let mut last_line = 0;
334 let mut last_end: usize = 0;
335 let mut offset = 0;
336 let mut glyph_tail_found = false;
337 let mut last_glyph: Option<&LayoutGlyph> = None;
338 let mut last_position = HitPosition {
339 line: 0,
340 point: Point::ZERO,
341 glyph_ascent: 0.0,
342 glyph_descent: 0.0,
343 };
344 for (line, run) in self.layout_runs().enumerate() {
345 if run.line_i > last_line {
346 last_line = run.line_i;
347 offset = last_end + 1;
348 }
349
350 if let Some(last_glyph) = last_glyph {
361 if let Some(first_glyph) = run.glyphs.first() {
362 let in_wrapped_tail = match affinity {
363 Affinity::Before => first_glyph.start + offset == idx,
365 Affinity::After => first_glyph.start + offset != idx && glyph_tail_found,
367 };
368
369 if in_wrapped_tail {
370 last_position.point.x += last_glyph.w as f64;
371
372 if last_end != idx {
373 last_position.point.x += last_glyph.w as f64;
376 }
377
378 last_position.point.y = 0.0;
379 return last_position;
380 }
381 }
382 }
383
384 for glyph in run.glyphs {
385 let glyph_start = glyph.start + offset;
386 let glyph_end = glyph.end + offset;
387
388 last_end = glyph_end;
389 last_glyph = Some(glyph);
390 last_position = HitPosition {
391 line,
392 point: Point::new(glyph.x as f64, run.line_y as f64),
393 glyph_ascent: run.max_ascent as f64,
394 glyph_descent: run.max_descent as f64,
395 };
396
397 glyph_tail_found = idx == glyph_end;
398
399 if (glyph_start..glyph_end).contains(&idx)
400 || (affinity == Affinity::Before && glyph_tail_found)
401 {
402 let glyph_str = &run.text[glyph.start..glyph.end];
405 let relative_idx = idx - glyph_start;
406 let mut total_graphemes = 0;
407 let mut grapheme_i = 0;
408
409 for (i, _) in glyph_str.grapheme_indices(true) {
410 if relative_idx > i {
411 grapheme_i += 1;
412 }
413
414 total_graphemes += 1;
415 }
416
417 if glyph.level.is_rtl() {
418 grapheme_i = total_graphemes - grapheme_i;
419 }
420
421 last_position.point.x +=
422 (grapheme_i as f64 / total_graphemes as f64) * glyph.w as f64;
423
424 return last_position;
425 }
426 }
427 }
428
429 if let Some(last_glyph) = last_glyph {
430 last_position.point.x += last_glyph.w as f64;
431 return last_position;
432 }
433
434 HitPosition {
435 line: 0,
436 point: Point::ZERO,
437 glyph_ascent: 0.0,
438 glyph_descent: 0.0,
439 }
440 }
441
442 pub fn hit_point(&self, point: Point) -> HitPoint {
443 if let Some(cursor) = self.hit(point.x as f32, point.y as f32) {
444 let size = self.size();
445 let is_inside = point.x <= size.width && point.y <= size.height;
446 HitPoint {
447 line: cursor.line,
448 index: cursor.index,
449 is_inside,
450 affinity: cursor.affinity,
451 }
452 } else {
453 HitPoint {
454 line: 0,
455 index: 0,
456 is_inside: false,
457 affinity: Affinity::Before,
458 }
459 }
460 }
461
462 pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
464 self.buffer.hit(x, y)
465 }
466
467 pub fn line_col_position(&self, line: usize, col: usize) -> HitPosition {
468 let mut last_glyph: Option<&LayoutGlyph> = None;
469 let mut last_line = 0;
470 let mut last_line_y = 0.0;
471 let mut last_glyph_ascent = 0.0;
472 let mut last_glyph_descent = 0.0;
473 for (current_line, run) in self.layout_runs().enumerate() {
474 for glyph in run.glyphs {
475 match run.line_i.cmp(&line) {
476 std::cmp::Ordering::Equal => {
477 if glyph.start > col {
478 return HitPosition {
479 line: last_line,
480 point: Point::new(
481 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
482 last_line_y as f64,
483 ),
484 glyph_ascent: last_glyph_ascent as f64,
485 glyph_descent: last_glyph_descent as f64,
486 };
487 }
488 if (glyph.start..glyph.end).contains(&col) {
489 return HitPosition {
490 line: current_line,
491 point: Point::new(glyph.x as f64, run.line_y as f64),
492 glyph_ascent: run.max_ascent as f64,
493 glyph_descent: run.max_descent as f64,
494 };
495 }
496 }
497 std::cmp::Ordering::Greater => {
498 return HitPosition {
499 line: last_line,
500 point: Point::new(
501 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
502 last_line_y as f64,
503 ),
504 glyph_ascent: last_glyph_ascent as f64,
505 glyph_descent: last_glyph_descent as f64,
506 };
507 }
508 std::cmp::Ordering::Less => {}
509 };
510 last_glyph = Some(glyph);
511 }
512 last_line = current_line;
513 last_line_y = run.line_y;
514 last_glyph_ascent = run.max_ascent;
515 last_glyph_descent = run.max_descent;
516 }
517
518 HitPosition {
519 line: last_line,
520 point: Point::new(
521 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
522 last_line_y as f64,
523 ),
524 glyph_ascent: last_glyph_ascent as f64,
525 glyph_descent: last_glyph_descent as f64,
526 }
527 }
528
529 pub fn size(&self) -> Size {
530 self.buffer
531 .layout_runs()
532 .fold(Size::new(0.0, 0.0), |mut size, run| {
533 let new_width = run.line_w as f64;
534 if new_width > size.width {
535 size.width = new_width;
536 }
537
538 size.height += run.line_height as f64;
539
540 size
541 })
542 }
543}