use pango::prelude::*;
use rotulus_layout::{Attrs, FontMetrics, Style, TextMeasure};
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
const CACHE_CAP: usize = 4096;
pub struct PangoMeasure {
context: pango::Context,
layout: pango::Layout,
font: pango::FontDescription,
metrics: FontMetrics,
zoom_permille: u32,
cache: RefCell<HashMap<u8, HashMap<String, u32>>>,
cache_len: Cell<usize>,
}
impl std::fmt::Debug for PangoMeasure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PangoMeasure")
.field("font", &self.font.to_str())
.field("metrics", &self.metrics)
.field("zoom_permille", &self.zoom_permille)
.finish()
}
}
impl PangoMeasure {
pub fn new(context: pango::Context, font: pango::FontDescription) -> PangoMeasure {
let layout = pango::Layout::new(&context);
let mut m = PangoMeasure {
context,
layout,
font,
metrics: FontMetrics::default(),
zoom_permille: 1000,
cache: RefCell::new(HashMap::new()),
cache_len: Cell::new(0),
};
m.recompute_metrics();
m
}
pub fn headless(font: &str) -> PangoMeasure {
let fm = pangocairo::FontMap::default();
let ctx = fm.create_context();
PangoMeasure::new(ctx, pango::FontDescription::from_string(font))
}
pub fn set_font(&mut self, font: pango::FontDescription) {
self.font = font;
self.clear_cache();
self.recompute_metrics();
}
pub fn set_zoom_permille(&mut self, zoom: u32) {
if zoom == self.zoom_permille {
return;
}
self.zoom_permille = zoom.clamp(250, 5000);
self.clear_cache();
self.recompute_metrics();
}
fn clear_cache(&self) {
self.cache.borrow_mut().clear();
self.cache_len.set(0);
}
pub fn zoom_permille(&self) -> u32 {
self.zoom_permille
}
pub fn context(&self) -> &pango::Context {
&self.context
}
pub fn scaled_font(&self) -> pango::FontDescription {
let mut f = self.font.clone();
let base = if f.size() > 0 {
f.size()
} else {
10 * pango::SCALE
};
let scaled = (i64::from(base) * i64::from(self.zoom_permille) / 1000) as i32;
f.set_size(scaled.max(pango::SCALE));
f
}
fn recompute_metrics(&mut self) {
let font = self.scaled_font();
self.context.set_font_description(&font);
let m = self.context.metrics(Some(&font), None);
let ascent = m.ascent() / pango::SCALE;
let descent = m.descent() / pango::SCALE;
let line_height = (ascent + descent).max(1);
self.layout.set_font_description(Some(&font));
self.layout.set_text(" ");
let (space_width, _) = self.layout.pixel_size();
self.metrics = FontMetrics {
line_height: line_height as u32,
ascent: ascent.max(0) as u32,
space_width: space_width.max(1) as u32,
};
}
pub fn attrs_for(style: Style) -> pango::AttrList {
let list = pango::AttrList::new();
if style.attrs.contains(Attrs::BOLD) {
list.insert(pango::AttrInt::new_weight(pango::Weight::Bold));
}
if style.attrs.contains(Attrs::ITALIC) {
list.insert(pango::AttrInt::new_style(pango::Style::Italic));
}
if style.attrs.contains(Attrs::UNDERLINE) {
list.insert(pango::AttrInt::new_underline(pango::Underline::Single));
}
if style.attrs.contains(Attrs::STRIKETHROUGH) {
list.insert(pango::AttrInt::new_strikethrough(true));
}
if style.attrs.contains(Attrs::CODE) {
list.insert(pango::AttrFontDesc::new(
&pango::FontDescription::from_string("Monospace"),
));
}
list
}
fn prepare(&self, text: &str, style: Style) -> &pango::Layout {
self.layout.set_font_description(Some(&self.scaled_font()));
self.layout.set_attributes(Some(&Self::attrs_for(style)));
self.layout.set_text(text);
&self.layout
}
}
impl TextMeasure for PangoMeasure {
fn metrics(&self) -> FontMetrics {
self.metrics
}
fn run_width(&self, text: &str, style: Style) -> u32 {
if text.is_empty() {
return 0;
}
let attrs = style.attrs.0;
if let Some(w) = self
.cache
.borrow()
.get(&attrs)
.and_then(|inner| inner.get(text))
{
return *w;
}
let layout = self.prepare(text, style);
let (w, _) = layout.pixel_size();
let w = w.max(0) as u32;
{
let mut c = self.cache.borrow_mut();
if self.cache_len.get() >= CACHE_CAP {
c.clear();
self.cache_len.set(0);
}
if c.entry(attrs)
.or_default()
.insert(text.to_string(), w)
.is_none()
{
self.cache_len.set(self.cache_len.get() + 1);
}
}
w
}
fn fit_prefix(&self, text: &str, style: Style, max_width: u32) -> (usize, u32) {
if text.is_empty() || max_width == 0 {
return (0, 0);
}
let layout = self.prepare(text, style);
let (full, _) = layout.pixel_size();
if full.max(0) as u32 <= max_width {
return (text.len(), full.max(0) as u32);
}
let line = match layout.line_readonly(0) {
Some(l) => l,
None => return self.min_progress(text, style),
};
let hit = line.x_to_index((max_width as i32) * pango::SCALE);
let mut idx = if hit.is_inside() {
(hit.index() as usize).min(text.len())
} else {
text.len()
};
while idx > 0 && !text.is_char_boundary(idx) {
idx -= 1;
}
if idx == 0 {
return self.min_progress(text, style);
}
(idx, self.run_width(&text[..idx], style))
}
}
impl PangoMeasure {
fn min_progress(&self, text: &str, style: Style) -> (usize, u32) {
if text.is_empty() {
return (0, 0);
}
let mut i = 1;
while i < text.len() && !text.is_char_boundary(i) {
i += 1;
}
(i, self.run_width(&text[..i], style))
}
}