use std::{ops::Range, sync::LazyLock};
use crate::text::AttrsList;
use cosmic_text::{
Affinity, Buffer, BufferLine, Cursor, FontSystem, LayoutCursor, LayoutGlyph, LineEnding,
LineIter, Metrics, Scroll, Shaping, Wrap,
};
use parking_lot::Mutex;
use peniko::kurbo::{Point, Size};
pub static FONT_SYSTEM: LazyLock<Mutex<FontSystem>> = LazyLock::new(|| {
let mut font_system = FontSystem::new();
#[cfg(target_os = "macos")]
font_system.db_mut().set_sans_serif_family("Helvetica Neue");
#[cfg(target_os = "windows")]
font_system.db_mut().set_sans_serif_family("Segoe UI");
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
font_system.db_mut().set_sans_serif_family("Noto Sans");
Mutex::new(font_system)
});
#[derive(Debug)]
pub struct LayoutRun<'a> {
pub line_i: usize,
pub text: &'a str,
pub rtl: bool,
pub glyphs: &'a [LayoutGlyph],
pub max_ascent: f32,
pub max_descent: f32,
pub line_y: f32,
pub line_top: f32,
pub line_height: f32,
pub line_w: f32,
}
impl<'a> LayoutRun<'a> {
pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> {
let mut x_start = None;
let mut x_end = None;
let rtl_factor = if self.rtl { 1. } else { 0. };
let ltr_factor = 1. - rtl_factor;
for glyph in self.glyphs.iter() {
let cursor = self.cursor_from_glyph_left(glyph);
if cursor >= cursor_start && cursor <= cursor_end {
if x_start.is_none() {
x_start = Some(glyph.x + glyph.w * rtl_factor);
}
x_end = Some(glyph.x + glyph.w * rtl_factor);
}
let cursor = self.cursor_from_glyph_right(glyph);
if cursor >= cursor_start && cursor <= cursor_end {
if x_start.is_none() {
x_start = Some(glyph.x + glyph.w * ltr_factor);
}
x_end = Some(glyph.x + glyph.w * ltr_factor);
}
}
if let Some(x_start) = x_start {
let x_end = x_end.expect("end of cursor not found");
let (x_start, x_end) = if x_start < x_end {
(x_start, x_end)
} else {
(x_end, x_start)
};
Some((x_start, x_end - x_start))
} else {
None
}
}
fn cursor_from_glyph_left(&self, glyph: &LayoutGlyph) -> Cursor {
if self.rtl {
Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
} else {
Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
}
}
fn cursor_from_glyph_right(&self, glyph: &LayoutGlyph) -> Cursor {
if self.rtl {
Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
} else {
Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
}
}
}
#[derive(Debug)]
pub struct LayoutRunIter<'b> {
text_layout: &'b TextLayout,
line_i: usize,
layout_i: usize,
total_height: f32,
line_top: f32,
}
impl<'b> LayoutRunIter<'b> {
pub fn new(text_layout: &'b TextLayout) -> Self {
Self {
text_layout,
line_i: text_layout.buffer.scroll().line,
layout_i: 0,
total_height: 0.0,
line_top: 0.0,
}
}
}
impl<'b> Iterator for LayoutRunIter<'b> {
type Item = LayoutRun<'b>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(line) = self.text_layout.buffer.lines.get(self.line_i) {
let shape = line.shape_opt().as_ref()?;
let layout = line.layout_opt().as_ref()?;
while let Some(layout_line) = layout.get(self.layout_i) {
self.layout_i += 1;
let line_height = layout_line
.line_height_opt
.unwrap_or(self.text_layout.buffer.metrics().line_height);
self.total_height += line_height;
let line_top = self.line_top - self.text_layout.buffer.scroll().vertical;
let glyph_height = layout_line.max_ascent + layout_line.max_descent;
let centering_offset = (line_height - glyph_height) / 2.0;
let line_y = line_top + centering_offset + layout_line.max_ascent;
if let Some(height) = self.text_layout.height_opt {
if line_y > height {
return None;
}
}
self.line_top += line_height;
if line_y < 0.0 {
continue;
}
return Some(LayoutRun {
line_i: self.line_i,
text: line.text(),
rtl: shape.rtl,
glyphs: &layout_line.glyphs,
max_ascent: layout_line.max_ascent,
max_descent: layout_line.max_descent,
line_y,
line_top,
line_height,
line_w: layout_line.w,
});
}
self.line_i += 1;
self.layout_i = 0;
}
None
}
}
pub struct HitPosition {
pub line: usize,
pub point: Point,
pub glyph_ascent: f64,
pub glyph_descent: f64,
}
pub struct HitPoint {
pub line: usize,
pub index: usize,
pub is_inside: bool,
}
#[derive(Clone, Debug)]
pub struct TextLayout {
buffer: Buffer,
lines_range: Vec<Range<usize>>,
width_opt: Option<f32>,
height_opt: Option<f32>,
}
impl Default for TextLayout {
fn default() -> Self {
Self::new()
}
}
impl TextLayout {
pub fn new() -> Self {
TextLayout {
buffer: Buffer::new_empty(Metrics::new(16.0, 16.0)),
lines_range: Vec::new(),
width_opt: None,
height_opt: None,
}
}
pub fn set_text(&mut self, text: &str, attrs_list: AttrsList) {
self.buffer.lines.clear();
self.lines_range.clear();
let mut attrs_list = attrs_list.0;
for (range, ending) in LineIter::new(text) {
self.lines_range.push(range.clone());
let line_text = &text[range];
let new_attrs = attrs_list
.clone()
.split_off(line_text.len() + ending.as_str().len());
self.buffer.lines.push(BufferLine::new(
line_text,
ending,
attrs_list.clone(),
Shaping::Advanced,
));
attrs_list = new_attrs;
}
if self.buffer.lines.is_empty() {
self.buffer.lines.push(BufferLine::new(
"",
LineEnding::default(),
attrs_list,
Shaping::Advanced,
));
self.lines_range.push(0..0)
}
self.buffer.set_scroll(Scroll::default());
let mut font_system = FONT_SYSTEM.lock();
self.buffer.shape_until_scroll(&mut font_system, false);
}
pub fn set_wrap(&mut self, wrap: Wrap) {
let mut font_system = FONT_SYSTEM.lock();
self.buffer.set_wrap(&mut font_system, wrap);
}
pub fn set_tab_width(&mut self, tab_width: usize) {
let mut font_system = FONT_SYSTEM.lock();
self.buffer
.set_tab_width(&mut font_system, tab_width as u16);
}
pub fn set_size(&mut self, width: f32, height: f32) {
let mut font_system = FONT_SYSTEM.lock();
self.width_opt = Some(width);
self.height_opt = Some(height);
self.buffer
.set_size(&mut font_system, Some(width), Some(height));
}
pub fn lines(&self) -> &[BufferLine] {
&self.buffer.lines
}
pub fn lines_range(&self) -> &[Range<usize>] {
&self.lines_range
}
pub fn layout_runs(&self) -> LayoutRunIter {
LayoutRunIter::new(self)
}
pub fn layout_cursor(&mut self, cursor: Cursor) -> LayoutCursor {
let line = cursor.line;
let mut font_system = FONT_SYSTEM.lock();
self.buffer
.layout_cursor(&mut font_system, cursor)
.unwrap_or_else(|| LayoutCursor::new(line, 0, 0))
}
pub fn hit_position(&self, idx: usize) -> HitPosition {
let mut last_line = 0;
let mut last_end: usize = 0;
let mut offset = 0;
let mut last_glyph_width = 0.0;
let mut last_position = HitPosition {
line: 0,
point: Point::ZERO,
glyph_ascent: 0.0,
glyph_descent: 0.0,
};
for (line, run) in self.layout_runs().enumerate() {
if run.line_i > last_line {
last_line = run.line_i;
offset += last_end + 1;
}
for glyph in run.glyphs {
if glyph.start + offset > idx {
last_position.point.x += last_glyph_width as f64;
return last_position;
}
last_end = glyph.end;
last_glyph_width = glyph.w;
last_position = HitPosition {
line,
point: Point::new(glyph.x as f64, run.line_y as f64),
glyph_ascent: run.max_ascent as f64,
glyph_descent: run.max_descent as f64,
};
if (glyph.start + offset..glyph.end + offset).contains(&idx) {
return last_position;
}
}
}
if idx > 0 {
last_position.point.x += last_glyph_width as f64;
return last_position;
}
HitPosition {
line: 0,
point: Point::ZERO,
glyph_ascent: 0.0,
glyph_descent: 0.0,
}
}
pub fn hit_point(&self, point: Point) -> HitPoint {
if let Some(cursor) = self.hit(point.x as f32, point.y as f32) {
let size = self.size();
let is_inside = point.x <= size.width && point.y <= size.height;
HitPoint {
line: cursor.line,
index: cursor.index,
is_inside,
}
} else {
HitPoint {
line: 0,
index: 0,
is_inside: false,
}
}
}
pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
self.buffer.hit(x, y)
}
pub fn line_col_position(&self, line: usize, col: usize) -> HitPosition {
let mut last_glyph: Option<&LayoutGlyph> = None;
let mut last_line = 0;
let mut last_line_y = 0.0;
let mut last_glyph_ascent = 0.0;
let mut last_glyph_descent = 0.0;
for (current_line, run) in self.layout_runs().enumerate() {
for glyph in run.glyphs {
match run.line_i.cmp(&line) {
std::cmp::Ordering::Equal => {
if glyph.start > col {
return HitPosition {
line: last_line,
point: Point::new(
last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
last_line_y as f64,
),
glyph_ascent: last_glyph_ascent as f64,
glyph_descent: last_glyph_descent as f64,
};
}
if (glyph.start..glyph.end).contains(&col) {
return HitPosition {
line: current_line,
point: Point::new(glyph.x as f64, run.line_y as f64),
glyph_ascent: run.max_ascent as f64,
glyph_descent: run.max_descent as f64,
};
}
}
std::cmp::Ordering::Greater => {
return HitPosition {
line: last_line,
point: Point::new(
last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
last_line_y as f64,
),
glyph_ascent: last_glyph_ascent as f64,
glyph_descent: last_glyph_descent as f64,
};
}
std::cmp::Ordering::Less => {}
};
last_glyph = Some(glyph);
}
last_line = current_line;
last_line_y = run.line_y;
last_glyph_ascent = run.max_ascent;
last_glyph_descent = run.max_descent;
}
HitPosition {
line: last_line,
point: Point::new(
last_glyph.map(|g| (g.x + g.w) as f64).unwrap_or(0.0),
last_line_y as f64,
),
glyph_ascent: last_glyph_ascent as f64,
glyph_descent: last_glyph_descent as f64,
}
}
pub fn size(&self) -> Size {
self.buffer
.layout_runs()
.fold(Size::new(0.0, 0.0), |mut size, run| {
let new_width = run.line_w as f64;
if new_width > size.width {
size.width = new_width;
}
size.height += run.line_height as f64;
size
})
}
}