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};
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()?;
131 let layout = line.layout_opt()?;
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, align: Option<Align>) -> Self {
225 let mut layout = Self::new();
226 layout.set_text(text, attrs_list, align);
227 layout
228 }
229
230 pub fn set_text(&mut self, text: &str, attrs_list: AttrsList, align: Option<Align>) {
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 let mut line =
241 BufferLine::new(line_text, ending, attrs_list.clone(), Shaping::Advanced);
242 line.set_align(align);
243 self.buffer.lines.push(line);
244 attrs_list = new_attrs;
245 }
246 if self.buffer.lines.is_empty() {
247 let mut line =
248 BufferLine::new("", LineEnding::default(), attrs_list, Shaping::Advanced);
249 line.set_align(align);
250 self.buffer.lines.push(line);
251 self.lines_range.push(0..0)
252 }
253 self.buffer.set_scroll(Scroll::default());
254
255 let mut font_system = FONT_SYSTEM.lock();
256
257 let needs_two_pass =
259 align.is_some() && align != Some(Align::Left) && self.width_opt.is_none();
260 if needs_two_pass {
261 self.buffer.shape_until_scroll(&mut font_system, false);
263
264 let measured_width = self
266 .buffer
267 .layout_runs()
268 .fold(0.0f32, |width, run| width.max(run.line_w));
269
270 if measured_width > 0.0 {
272 self.buffer
273 .set_size(&mut font_system, Some(measured_width), self.height_opt);
274 self.buffer.shape_until_scroll(&mut font_system, false);
276 }
277 } else {
278 self.buffer.shape_until_scroll(&mut font_system, false);
280 }
281 }
282
283 pub fn set_wrap(&mut self, wrap: Wrap) {
284 let mut font_system = FONT_SYSTEM.lock();
285 self.buffer.set_wrap(&mut font_system, wrap);
286 }
287
288 pub fn set_tab_width(&mut self, tab_width: usize) {
289 let mut font_system = FONT_SYSTEM.lock();
290 self.buffer
291 .set_tab_width(&mut font_system, tab_width as u16);
292 }
293
294 pub fn set_size(&mut self, width: f32, height: f32) {
295 let mut font_system = FONT_SYSTEM.lock();
296 self.width_opt = Some(width);
297 self.height_opt = Some(height);
298 self.buffer
299 .set_size(&mut font_system, Some(width), Some(height));
300 }
301
302 pub fn metrics(&self) -> Metrics {
303 self.buffer.metrics()
304 }
305
306 pub fn lines(&self) -> &[BufferLine] {
307 &self.buffer.lines
308 }
309
310 pub fn lines_range(&self) -> &[Range<usize>] {
311 &self.lines_range
312 }
313
314 pub fn layout_runs(&self) -> LayoutRunIter {
315 LayoutRunIter::new(self)
316 }
317
318 pub fn layout_cursor(&mut self, cursor: Cursor) -> LayoutCursor {
319 let line = cursor.line;
320 let mut font_system = FONT_SYSTEM.lock();
321 self.buffer
322 .layout_cursor(&mut font_system, cursor)
323 .unwrap_or_else(|| LayoutCursor::new(line, 0, 0))
324 }
325
326 pub fn hit_position(&self, idx: usize) -> HitPosition {
327 let mut last_line = 0;
328 let mut last_end: usize = 0;
329 let mut offset = 0;
330 let mut last_glyph_width = 0.0;
331 let mut last_position = HitPosition {
332 line: 0,
333 point: Point::ZERO,
334 glyph_ascent: 0.0,
335 glyph_descent: 0.0,
336 };
337 for (line, run) in self.layout_runs().enumerate() {
338 if run.line_i > last_line {
339 last_line = run.line_i;
340 offset += last_end + 1;
341 }
342 for glyph in run.glyphs {
343 if glyph.start + offset > idx {
344 last_position.point.x += last_glyph_width as f64;
345 return last_position;
346 }
347 last_end = glyph.end;
348 last_glyph_width = glyph.w;
349 last_position = HitPosition {
350 line,
351 point: Point::new(glyph.x as f64, run.line_y as f64),
352 glyph_ascent: run.max_ascent as f64,
353 glyph_descent: run.max_descent as f64,
354 };
355 if (glyph.start + offset..glyph.end + offset).contains(&idx) {
356 return last_position;
357 }
358 }
359 }
360
361 if idx > 0 {
362 last_position.point.x += last_glyph_width as f64;
363 return last_position;
364 }
365
366 HitPosition {
367 line: 0,
368 point: Point::ZERO,
369 glyph_ascent: 0.0,
370 glyph_descent: 0.0,
371 }
372 }
373
374 pub fn hit_point(&self, point: Point) -> HitPoint {
375 if let Some(cursor) = self.hit(point.x as f32, point.y as f32) {
376 let size = self.size();
377 let is_inside = point.x <= size.width && point.y <= size.height;
378 HitPoint {
379 line: cursor.line,
380 index: cursor.index,
381 is_inside,
382 }
383 } else {
384 HitPoint {
385 line: 0,
386 index: 0,
387 is_inside: false,
388 }
389 }
390 }
391
392 pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
394 self.buffer.hit(x, y)
395 }
396
397 pub fn line_col_position(&self, line: usize, col: usize) -> HitPosition {
398 let mut last_glyph: Option<&LayoutGlyph> = None;
399 let mut last_line = 0;
400 let mut last_line_y = 0.0;
401 let mut last_glyph_ascent = 0.0;
402 let mut last_glyph_descent = 0.0;
403 for (current_line, run) in self.layout_runs().enumerate() {
404 for glyph in run.glyphs {
405 match run.line_i.cmp(&line) {
406 std::cmp::Ordering::Equal => {
407 if glyph.start > col {
408 return HitPosition {
409 line: last_line,
410 point: Point::new(
411 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
412 last_line_y as f64,
413 ),
414 glyph_ascent: last_glyph_ascent as f64,
415 glyph_descent: last_glyph_descent as f64,
416 };
417 }
418 if (glyph.start..glyph.end).contains(&col) {
419 return HitPosition {
420 line: current_line,
421 point: Point::new(glyph.x as f64, run.line_y as f64),
422 glyph_ascent: run.max_ascent as f64,
423 glyph_descent: run.max_descent as f64,
424 };
425 }
426 }
427 std::cmp::Ordering::Greater => {
428 return HitPosition {
429 line: last_line,
430 point: Point::new(
431 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
432 last_line_y as f64,
433 ),
434 glyph_ascent: last_glyph_ascent as f64,
435 glyph_descent: last_glyph_descent as f64,
436 };
437 }
438 std::cmp::Ordering::Less => {}
439 };
440 last_glyph = Some(glyph);
441 }
442 last_line = current_line;
443 last_line_y = run.line_y;
444 last_glyph_ascent = run.max_ascent;
445 last_glyph_descent = run.max_descent;
446 }
447
448 HitPosition {
449 line: last_line,
450 point: Point::new(
451 last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
452 last_line_y as f64,
453 ),
454 glyph_ascent: last_glyph_ascent as f64,
455 glyph_descent: last_glyph_descent as f64,
456 }
457 }
458
459 pub fn size(&self) -> Size {
460 self.buffer
461 .layout_runs()
462 .fold(Size::new(0.0, 0.0), |mut size, run| {
463 let new_width = run.line_w as f64;
464 if new_width > size.width {
465 size.width = new_width;
466 }
467
468 size.height += run.line_height as f64;
469
470 size
471 })
472 }
473}