use cosmic_text::{AttrsList, Buffer, BufferLine, FontSystem, LineEnding};
use crate::geom::Rect;
fn opens(c: char) -> bool {
matches!(
c,
'"' | '\''
| '('
| '['
| '{'
| '¿'
| '¡'
| '\u{201c}' | '\u{2018}' | '\u{201e}' | '\u{201a}' | '\u{ab}' | '\u{2039}' | '\u{300c}' | '\u{300e}' )
}
pub(crate) fn initial_len(text: &str) -> Option<usize> {
use unicode_segmentation::UnicodeSegmentation;
let mut at = 0;
for g in text.graphemes(true).take(4) {
let c = g.chars().next()?;
if c.is_alphanumeric() {
return Some(at + g.len());
}
if !opens(c) {
return None;
}
at += g.len();
}
None
}
#[derive(Clone)]
pub(crate) struct Initial {
pub(crate) lines: u32,
gap: f32,
cut: Option<(usize, BufferLine)>,
beside: usize,
pub(crate) indent: f32,
drop: f32,
lift: f32,
pub(crate) rect: Rect,
beside_bottom: f32,
}
fn cap_ratio(
buffer: &Buffer,
fs: &mut FontSystem,
line: usize,
pick: impl Fn(usize) -> bool,
) -> Option<f32> {
let run = buffer.layout_runs().find(|r| r.line_i == line)?;
let g = run.glyphs.iter().rev().find(|g| pick(g.start))?;
let font = fs.get_font(g.font_id, g.font_weight)?;
let m = font.as_swash().metrics(&[]);
(m.cap_height > 0.0 && m.units_per_em > 0).then(|| m.cap_height / m.units_per_em as f32)
}
const COLLAPSED: f32 = 1.0 / 1024.0;
fn collapse_lead(line: &mut BufferLine, line_height: f32) {
let n = line
.text()
.bytes()
.take_while(|b| matches!(b, b' ' | b'\t'))
.count();
if n == 0 {
return;
}
let m = cosmic_text::Metrics::new(COLLAPSED, line_height);
let mut list = line.attrs_list().clone();
for i in 0..n {
let attrs = list.get_span(i).metrics(m);
let attrs = cosmic_text::AttrsOwned::new(&attrs);
list.add_span(i..i + 1, &attrs.as_attrs());
}
line.set_attrs_list(list);
}
fn lay(buffer: &mut Buffer, fs: &mut FontSystem, i: usize, width: Option<f32>) -> usize {
let size = buffer.metrics().font_size;
let (wrap, ellipsize, mono, tab, hinting) = (
buffer.wrap(),
buffer.ellipsize(),
buffer.monospace_width(),
buffer.tab_width(),
buffer.hinting(),
);
let line = &mut buffer.lines[i];
line.reset_layout();
line.layout(fs, size, width, wrap, ellipsize, mono, tab, hinting)
.len()
}
impl Initial {
pub(crate) fn cut(
buffer: &mut Buffer,
fs: &mut FontSystem,
lines: u32,
gap: f32,
) -> Option<Self> {
let k = initial_len(buffer.lines.first()?.text())?;
let metrics = buffer.metrics();
let own = cap_ratio(buffer, fs, 0, |b| b < k).unwrap_or(0.7);
let text = cap_ratio(buffer, fs, 0, |b| b >= k).unwrap_or(own);
let cap = (lines - 1) as f32 * metrics.line_height + text * metrics.font_size;
let size = (cap / own).max(1.0);
let first = &mut buffer.lines[0];
if k < first.text().len() {
let mut rest = first.split_off(k);
collapse_lead(&mut rest, metrics.line_height);
buffer.lines.insert(1, rest);
}
let m = crate::text::shaper_metrics(size, size.round());
let old = buffer.lines[0].attrs_list().clone();
let mut list = AttrsList::new(&old.defaults().metrics(m));
for (range, attrs) in old.spans_iter() {
list.add_span(range.clone(), &attrs.as_attrs().metrics(m));
}
buffer.lines[0].set_attrs_list(list);
let width = buffer.size().0;
lay(buffer, fs, 0, width);
let mut initial = Self {
lines,
gap,
cut: None,
beside: 1,
indent: 0.0,
drop: 0.0,
lift: 0.0,
rect: Rect::default(),
beside_bottom: 0.0,
};
initial.place(buffer, fs);
Some(initial)
}
pub(crate) fn uncut(&mut self, buffer: &mut Buffer) {
if let Some((at, whole)) = self.cut.take() {
buffer.lines.splice(at..at + 2, [whole]);
}
}
pub(crate) fn place(&mut self, buffer: &mut Buffer, fs: &mut FontSystem) {
let width = buffer.size().0;
for i in 0..buffer.lines.len() {
if buffer.lines[i].layout_opt().is_none() {
lay(buffer, fs, i, if i == 0 { None } else { width });
}
}
let Some((w0, h0, y0)) = buffer
.layout_runs()
.next()
.map(|r| (r.line_w, r.line_height, r.line_y))
else {
return;
};
self.indent = (w0 + self.gap).ceil();
self.lift = h0.round();
let narrow = width.map(|w| (w - self.indent).max(1.0));
let want = self.lines as usize;
let (mut rows, mut i) = (0usize, 1usize);
self.beside = buffer.lines.len();
while i < buffer.lines.len() {
let n = lay(buffer, fs, i, narrow);
if rows + n >= want {
let keep = want - rows;
let at = buffer.lines[i]
.layout_opt()
.and_then(|l| l.get(keep))
.and_then(|l| l.glyphs.iter().map(|g| g.start).min())
.filter(|&at| at > 0 && at < buffer.lines[i].text().len());
if let Some(at) = at {
let whole = buffer.lines[i].clone();
let rest = buffer.lines[i].split_off(at);
buffer.lines.insert(i + 1, rest);
lay(buffer, fs, i, narrow);
lay(buffer, fs, i + 1, width);
self.cut = Some((i, whole));
}
self.beside = i + 1;
break;
}
rows += n;
i += 1;
}
let lh = buffer.metrics().line_height;
let (mut count, mut nth, mut bottom) = (0usize, None, 0.0f32);
for run in buffer.layout_runs().skip(1) {
if run.line_i >= self.beside {
break;
}
count += 1;
let top = run.line_top - self.lift;
if count <= want {
nth = Some((top, run.line_y.round() - self.lift, run.line_height));
}
bottom = top + run.line_height;
}
self.beside_bottom = bottom;
let more = want.saturating_sub(count.max(1)) as f32 * lh;
let (top, base, h) = nth.unwrap_or((0.0, (lh * 0.8).round(), lh));
self.drop = (base + more - y0.round()).round();
self.rect = Rect::new(0.0, 0.0, w0, top + h + more);
}
pub(crate) fn shift(&self, line_i: usize) -> (f32, f32) {
if line_i == 0 {
(0.0, self.drop)
} else if line_i < self.beside {
(self.indent, -self.lift)
} else {
(0.0, -self.lift)
}
}
pub(crate) fn to_buffer(&self, x: f32, y: f32) -> (f32, f32) {
if x < self.rect.w + self.gap / 2.0 && y < self.rect.h.max(self.beside_bottom) {
return (x, self.lift / 2.0);
}
let x = if y < self.beside_bottom {
x - self.indent
} else {
x
};
(x, (y + self.lift).max(self.lift))
}
}
pub(crate) fn joined(buffer: &Buffer, i: usize) -> bool {
i + 1 < buffer.lines.len() && buffer.lines[i].ending() == LineEnding::None
}
#[cfg(test)]
mod tests {
use super::initial_len;
#[test]
fn the_initial_is_the_first_letter_with_its_opening_punctuation() {
assert_eq!(initial_len("Once upon"), Some(1));
assert_eq!(initial_len("“Once”"), Some(4));
assert_eq!(initial_len("¿Qué?"), Some(3));
assert_eq!(initial_len("Éire"), Some(2));
assert_eq!(initial_len("1984"), Some(1));
assert_eq!(initial_len(" Once"), None);
assert_eq!(initial_len("— Once"), None);
assert_eq!(initial_len(""), None);
assert_eq!(initial_len("\"\"\"\"a"), None);
}
}