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 let mut last_line = 0;
330 let mut last_end: usize = 0;
331 let mut offset = 0;
332 let mut last_glyph_width = 0.0;
333 let mut last_position = HitPosition {
334 line: 0,
335 point: Point::ZERO,
336 glyph_ascent: 0.0,
337 glyph_descent: 0.0,
338 };
339 for (line, run) in self.layout_runs().enumerate() {
340 if run.line_i > last_line {
341 last_line = run.line_i;
342 offset += last_end + 1;
343 }
344 for glyph in run.glyphs {
345 last_end = glyph.end;
346 last_glyph_width = glyph.w;
347 last_position = HitPosition {
348 line,
349 point: Point::new(glyph.x as f64, run.line_y as f64),
350 glyph_ascent: run.max_ascent as f64,
351 glyph_descent: run.max_descent as f64,
352 };
353 if (glyph.start + offset..=glyph.end + offset).contains(&idx) {
354 let glyph_str = &run.text[glyph.start..glyph.end];
357 let relative_idx = idx - offset - glyph.start;
358 let mut total_graphemes = 0;
359 let mut grapheme_i = 0;
360
361 for (i, _) in glyph_str.grapheme_indices(true) {
362 if relative_idx > i {
363 grapheme_i += 1;
364 }
365
366 total_graphemes += 1;
367 }
368
369 if glyph.level.is_rtl() {
370 grapheme_i = total_graphemes - grapheme_i;
371 }
372
373 last_position.point.x +=
374 (grapheme_i as f64 / total_graphemes as f64) * glyph.w as f64;
375
376 return last_position;
377 }
378 }
379 }
380
381 if idx > 0 {
382 last_position.point.x += last_glyph_width as f64;
383 return last_position;
384 }
385
386 HitPosition {
387 line: 0,
388 point: Point::ZERO,
389 glyph_ascent: 0.0,
390 glyph_descent: 0.0,
391 }
392 }
393
394 pub fn hit_point(&self, point: Point) -> HitPoint {
395 if let Some(cursor) = self.hit(point.x as f32, point.y as f32) {
396 let size = self.size();
397 let is_inside = point.x <= size.width && point.y <= size.height;
398 HitPoint {
399 line: cursor.line,
400 index: cursor.index,
401 is_inside,
402 affinity: cursor.affinity,
403 }
404 } else {
405 HitPoint {
406 line: 0,
407 index: 0,
408 is_inside: false,
409 affinity: Affinity::Before,
410 }
411 }
412 }
413
414 pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
416 self.buffer.hit(x, y)
417 }
418
419 pub fn line_col_position(&self, line: usize, col: usize) -> HitPosition {
420 let mut last_glyph: Option<&LayoutGlyph> = None;
421 let mut last_line = 0;
422 let mut last_line_y = 0.0;
423 let mut last_glyph_ascent = 0.0;
424 let mut last_glyph_descent = 0.0;
425 for (current_line, run) in self.layout_runs().enumerate() {
426 for glyph in run.glyphs {
427 match run.line_i.cmp(&line) {
428 std::cmp::Ordering::Equal => {
429 if glyph.start > col {
430 return HitPosition {
431 line: last_line,
432 point: Point::new(
433 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
434 last_line_y as f64,
435 ),
436 glyph_ascent: last_glyph_ascent as f64,
437 glyph_descent: last_glyph_descent as f64,
438 };
439 }
440 if (glyph.start..glyph.end).contains(&col) {
441 return HitPosition {
442 line: current_line,
443 point: Point::new(glyph.x as f64, run.line_y as f64),
444 glyph_ascent: run.max_ascent as f64,
445 glyph_descent: run.max_descent as f64,
446 };
447 }
448 }
449 std::cmp::Ordering::Greater => {
450 return HitPosition {
451 line: last_line,
452 point: Point::new(
453 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
454 last_line_y as f64,
455 ),
456 glyph_ascent: last_glyph_ascent as f64,
457 glyph_descent: last_glyph_descent as f64,
458 };
459 }
460 std::cmp::Ordering::Less => {}
461 };
462 last_glyph = Some(glyph);
463 }
464 last_line = current_line;
465 last_line_y = run.line_y;
466 last_glyph_ascent = run.max_ascent;
467 last_glyph_descent = run.max_descent;
468 }
469
470 HitPosition {
471 line: last_line,
472 point: Point::new(
473 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
474 last_line_y as f64,
475 ),
476 glyph_ascent: last_glyph_ascent as f64,
477 glyph_descent: last_glyph_descent as f64,
478 }
479 }
480
481 pub fn size(&self) -> Size {
482 self.buffer
483 .layout_runs()
484 .fold(Size::new(0.0, 0.0), |mut size, run| {
485 let new_width = run.line_w as f64;
486 if new_width > size.width {
487 size.width = new_width;
488 }
489
490 size.height += run.line_height as f64;
491
492 size
493 })
494 }
495}