use std::hash::{Hash, Hasher};
use cranpose_core::hash::default;
#[derive(Debug, Clone)]
pub struct LineLayout {
pub start_offset: usize,
pub end_offset: usize,
pub y: f32,
pub height: f32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GlyphLayout {
pub line_index: usize,
pub start_offset: usize,
pub end_offset: usize,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone)]
pub struct TextLayoutData {
pub width: f32,
pub height: f32,
pub line_height: f32,
pub glyph_x_positions: Vec<f32>,
pub char_to_byte: Vec<usize>,
pub lines: Vec<LineLayout>,
pub glyph_layouts: Vec<GlyphLayout>,
}
#[derive(Debug, Clone)]
pub struct TextLayoutResult {
pub width: f32,
pub height: f32,
pub line_height: f32,
glyph_x_positions: Vec<f32>,
char_to_byte: Vec<usize>,
pub lines: Vec<LineLayout>,
glyph_layouts: Vec<GlyphLayout>,
text_hash: u64,
}
impl TextLayoutResult {
pub fn new(text: &str, data: TextLayoutData) -> Self {
Self {
width: data.width,
height: data.height,
line_height: data.line_height,
glyph_x_positions: data.glyph_x_positions,
char_to_byte: data.char_to_byte,
lines: data.lines,
glyph_layouts: data.glyph_layouts,
text_hash: Self::hash_text(text),
}
}
pub fn get_cursor_x(&self, byte_offset: usize) -> f32 {
let char_idx = self
.char_to_byte
.iter()
.position(|&b| b > byte_offset)
.map(|i| i.saturating_sub(1))
.unwrap_or(self.char_to_byte.len().saturating_sub(1));
self.glyph_x_positions
.get(char_idx)
.copied()
.unwrap_or(self.width)
}
pub fn get_offset_for_x(&self, x: f32) -> usize {
if self.glyph_x_positions.is_empty() {
return 0;
}
let char_idx = match self
.glyph_x_positions
.binary_search_by(|pos| pos.partial_cmp(&x).unwrap_or(std::cmp::Ordering::Equal))
{
Ok(i) => i,
Err(i) => {
if i == 0 {
0
} else if i >= self.glyph_x_positions.len() {
self.glyph_x_positions.len() - 1
} else {
let before = self.glyph_x_positions[i - 1];
let after = self.glyph_x_positions[i];
if (x - before) < (after - x) { i - 1 } else { i }
}
}
};
self.char_to_byte.get(char_idx).copied().unwrap_or(0)
}
pub fn is_valid_for(&self, text: &str) -> bool {
self.text_hash == Self::hash_text(text)
}
pub fn glyph_layouts(&self) -> &[GlyphLayout] {
&self.glyph_layouts
}
fn hash_text(text: &str) -> u64 {
let mut hasher = default::new();
text.hash(&mut hasher);
hasher.finish()
}
pub fn monospaced(text: &str, char_width: f32, line_height: f32) -> Self {
let mut glyph_x_positions = Vec::new();
let mut char_to_byte = Vec::new();
let mut glyph_layouts = Vec::new();
let mut cursor_x = 0.0;
for (byte_offset, _c) in text.char_indices() {
glyph_x_positions.push(cursor_x);
char_to_byte.push(byte_offset);
cursor_x += char_width;
}
glyph_x_positions.push(cursor_x);
char_to_byte.push(text.len());
let mut line_x = 0.0;
let mut line_y = 0.0;
let mut line_index = 0usize;
for (byte_offset, c) in text.char_indices() {
if c == '\n' {
line_index = line_index.saturating_add(1);
line_y += line_height;
line_x = 0.0;
continue;
}
let glyph_start = byte_offset;
let glyph_end = glyph_start + c.len_utf8();
glyph_layouts.push(GlyphLayout {
line_index,
start_offset: glyph_start,
end_offset: glyph_end,
x: line_x,
y: line_y,
width: char_width,
height: line_height,
});
line_x += char_width;
}
let line_texts: Vec<&str> = text.split('\n').collect();
let line_count = line_texts.len();
let mut lines = Vec::with_capacity(line_count);
let mut line_start = 0;
let mut y = 0.0;
let mut max_width: f32 = 0.0;
for (i, line_text) in line_texts.iter().enumerate() {
let line_end = if i == line_count - 1 {
text.len()
} else {
line_start + line_text.len()
};
let line_width = line_text.chars().count() as f32 * char_width;
max_width = max_width.max(line_width);
lines.push(LineLayout {
start_offset: line_start,
end_offset: line_end,
y,
height: line_height,
});
line_start = line_end + 1;
y += line_height;
}
if lines.is_empty() {
lines.push(LineLayout {
start_offset: 0,
end_offset: 0,
y: 0.0,
height: line_height,
});
}
Self::new(
text,
TextLayoutData {
width: max_width,
height: lines.len() as f32 * line_height,
line_height,
glyph_x_positions,
char_to_byte,
lines,
glyph_layouts,
},
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_monospaced_layout() {
let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
assert_eq!(layout.get_cursor_x(0), 0.0);
assert_eq!(layout.get_cursor_x(5), 50.0);
}
#[test]
fn test_get_offset_for_x() {
let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
let offset = layout.get_offset_for_x(25.0);
assert!(offset == 2 || offset == 3);
}
#[test]
fn test_multiline() {
let layout = TextLayoutResult::monospaced("Hi\nWorld", 10.0, 20.0);
assert_eq!(layout.lines.len(), 2);
assert_eq!(layout.lines[0].start_offset, 0);
assert_eq!(layout.lines[1].start_offset, 3);
}
#[test]
fn test_validity() {
let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
assert!(layout.is_valid_for("Hello"));
assert!(!layout.is_valid_for("World"));
}
}