use crate::preview::mermaid::flowchart;
use crate::preview::mermaid::text_metrics::{self, FONT_SIZE};
pub const LINE_HEIGHT_RATIO: f64 = 1.1;
pub const ORTHO_LINE_GAP: f64 = 6.0;
pub fn ortho_line_pitch() -> f64 {
FONT_SIZE as f64 + ORTHO_LINE_GAP
}
pub const BASELINE_RATIO: f64 = 0.35;
pub fn line_height() -> f64 {
FONT_SIZE as f64 * LINE_HEIGHT_RATIO
}
#[derive(Debug, Clone, PartialEq)]
pub struct Label {
pub lines: Vec<String>,
pub width: f64,
pub height: f64,
pub line_pitch: f64,
pub font_size: f64,
}
impl Label {
pub fn measure(text: &str) -> Label {
Label::measure_at(text, FONT_SIZE as f64)
}
pub fn measure_at(text: &str, font_size: f64) -> Label {
Label::from_lines(author_lines(text), font_size, font_size * LINE_HEIGHT_RATIO)
}
pub fn measure_wrapped(text: &str, budget: f64, font_size: f64) -> Label {
let lines = author_lines(text)
.into_iter()
.flat_map(|line| wrap_line(&line, budget, font_size))
.collect();
Label::from_lines(lines, font_size, ortho_line_pitch())
}
fn from_lines(lines: Vec<String>, font_size: f64, line_pitch: f64) -> Label {
let width = lines
.iter()
.map(|l| text_metrics::measure(l, font_size as f32) as f64)
.fold(0.0_f64, f64::max);
Label {
height: lines.len() as f64 * line_pitch,
lines,
width,
line_pitch,
font_size,
}
}
pub fn is_blank(&self) -> bool {
self.lines.iter().all(|l| l.trim().is_empty())
}
pub fn resized(&self, font_size: f64) -> Label {
let scale = font_size / self.font_size;
Label {
lines: self.lines.clone(),
width: self.width * scale,
height: self.height * scale,
line_pitch: self.line_pitch * scale,
font_size,
}
}
pub fn line_height(&self) -> f64 {
self.line_pitch
}
pub fn baseline(&self, center_y: f64, i: usize) -> f64 {
let top = center_y - self.height / 2.0;
top + (i as f64 + 0.5) * self.line_height() + self.font_size * BASELINE_RATIO
}
}
fn author_lines(text: &str) -> Vec<String> {
let text = flowchart::text::replace_breaks(text);
let text = text.replace("\\n", "\n");
text.split('\n')
.map(|l| text_metrics::collapse_spaces(l).into_owned())
.collect()
}
fn breaks_anywhere(c: char) -> bool {
matches!(c,
'\u{1100}'..='\u{11ff}' | '\u{2e80}'..='\u{303f}' | '\u{3040}'..='\u{30ff}' | '\u{3400}'..='\u{4dbf}' | '\u{4e00}'..='\u{9fff}' | '\u{a960}'..='\u{a97f}' | '\u{ac00}'..='\u{d7ff}' | '\u{f900}'..='\u{faff}' | '\u{fe30}'..='\u{fe4f}' | '\u{ff00}'..='\u{ffef}' | '\u{20000}'..='\u{3ffff}' )
}
fn break_atoms(line: &str) -> Vec<&str> {
let mut atoms = Vec::new();
let mut word_start: Option<usize> = None;
for (i, c) in line.char_indices() {
if c == ' ' || breaks_anywhere(c) {
if let Some(s) = word_start.take() {
atoms.push(&line[s..i]);
}
atoms.push(&line[i..i + c.len_utf8()]);
} else if word_start.is_none() {
word_start = Some(i);
}
}
if let Some(s) = word_start {
atoms.push(&line[s..]);
}
atoms
}
fn wrap_line(line: &str, budget: f64, font_size: f64) -> Vec<String> {
if text_metrics::measure(line, font_size as f32) as f64 <= budget {
return vec![line.to_string()];
}
let mut out: Vec<String> = Vec::new();
let mut cur = String::new();
for atom in break_atoms(line) {
if cur.is_empty() && atom == " " {
continue;
}
let candidate = format!("{cur}{atom}");
if !cur.is_empty()
&& text_metrics::measure(candidate.trim_end(), font_size as f32) as f64 > budget
{
out.push(std::mem::take(&mut cur).trim_end().to_string());
if atom == " " {
continue;
}
cur.push_str(atom);
} else {
cur = candidate;
}
}
let cur = cur.trim_end();
if !cur.is_empty() || out.is_empty() {
out.push(cur.to_string());
}
out
}