use anyhow::Result;
use fontdb::{Database, Family, Query};
use swash::FontRef;
use crate::config::Config;
#[derive(Debug, Clone)]
pub struct FontMetrics {
pub cell_width: f32,
pub cell_height: f32,
pub ascent: f32,
pub descent: f32,
pub leading: f32,
pub char_advance: f32,
pub font_size_pixels: f32,
}
const EMBEDDED_FONT: &[u8] = include_bytes!("../fonts/DejaVuSansMono.ttf");
pub fn calculate_font_metrics(
font_family: Option<&str>,
font_size: f32,
line_spacing: f32,
char_spacing: f32,
scale_factor: f32,
) -> Result<FontMetrics> {
let mut font_db = Database::new();
font_db.load_system_fonts();
let font_data = if let Some(family_name) = font_family {
let query = Query {
families: &[Family::Name(family_name)],
weight: fontdb::Weight::NORMAL,
style: fontdb::Style::Normal,
..Query::default()
};
if let Some(id) = font_db.query(&query) {
if let Some((data, _)) = unsafe { font_db.make_shared_face_data(id) } {
data.as_ref().as_ref().to_vec()
} else {
log::warn!(
"Font '{}' found but failed to load data, using embedded font",
family_name
);
EMBEDDED_FONT.to_vec()
}
} else {
log::warn!(
"Font '{}' not found, using embedded DejaVu Sans Mono",
family_name
);
EMBEDDED_FONT.to_vec()
}
} else {
EMBEDDED_FONT.to_vec()
};
let font_ref = FontRef::from_index(&font_data, 0)
.ok_or_else(|| anyhow::anyhow!("Failed to create FontRef from font data"))?;
let platform_dpi = if cfg!(target_os = "macos") {
72.0
} else {
96.0
};
let base_font_pixels = font_size * platform_dpi / 72.0;
let font_size_pixels = (base_font_pixels * scale_factor).max(1.0);
let metrics = font_ref.metrics(&[]);
let scale = font_size_pixels / metrics.units_per_em as f32;
let ascent = metrics.ascent * scale;
let descent = metrics.descent * scale;
let leading = metrics.leading * scale;
let glyph_id = font_ref.charmap().map('m');
let char_advance = font_ref.glyph_metrics(&[]).advance_width(glyph_id) * scale;
let natural_line_height = ascent + descent + leading;
let cell_height = (natural_line_height * line_spacing).max(1.0);
let cell_width = (char_advance * char_spacing).max(1.0);
Ok(FontMetrics {
cell_width,
cell_height,
ascent,
descent,
leading,
char_advance,
font_size_pixels,
})
}
pub fn calculate_window_size(
cols: usize,
rows: usize,
cell_width: f32,
cell_height: f32,
padding: f32,
tab_bar_height: f32,
) -> (u32, u32) {
let content_width = cols as f32 * cell_width;
let content_height = rows as f32 * cell_height;
let width = (content_width + padding * 2.0).ceil() as u32;
let height = (content_height + padding * 2.0 + tab_bar_height).ceil() as u32;
(width.max(100), height.max(100)) }
pub fn window_size_from_config(config: &Config, scale_factor: f32) -> Result<(u32, u32)> {
let metrics = calculate_font_metrics(
Some(&config.font_family),
config.font_size,
config.line_spacing,
config.char_spacing,
scale_factor,
)?;
let tab_bar_height = match config.tab_bar_mode {
crate::config::TabBarMode::Always => config.tab_bar_height,
crate::config::TabBarMode::WhenMultiple | crate::config::TabBarMode::Never => 0.0,
};
let (width, height) = calculate_window_size(
config.cols,
config.rows,
metrics.cell_width,
metrics.cell_height,
config.window.window_padding,
tab_bar_height,
);
log::info!(
"Calculated window size: {}x{} for {}x{} grid (cell: {:.1}x{:.1}, padding: {:.1}, tab_bar: {:.1})",
width,
height,
config.cols,
config.rows,
metrics.cell_width,
metrics.cell_height,
config.window.window_padding,
tab_bar_height
);
Ok((width, height))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_font_metrics_embedded() {
let metrics = calculate_font_metrics(None, 13.0, 1.0, 1.0, 1.0)
.expect("embedded font metrics calculation should succeed");
assert!(metrics.cell_width > 0.0);
assert!(metrics.cell_height > 0.0);
assert!(metrics.font_size_pixels > 0.0);
}
#[test]
fn test_calculate_window_size() {
let (width, height) = calculate_window_size(80, 24, 8.0, 16.0, 10.0, 0.0);
assert_eq!(width, 660);
assert_eq!(height, 404);
}
#[test]
fn test_calculate_window_size_with_tab_bar() {
let (width, height) = calculate_window_size(80, 24, 8.0, 16.0, 10.0, 28.0);
assert_eq!(width, 660);
assert_eq!(height, 432);
}
#[test]
fn test_minimum_window_size() {
let (width, height) = calculate_window_size(1, 1, 1.0, 1.0, 0.0, 0.0);
assert_eq!(width, 100);
assert_eq!(height, 100);
}
#[test]
fn test_line_spacing_affects_cell_height() {
let metrics_tight = calculate_font_metrics(None, 13.0, 1.0, 1.0, 1.0)
.expect("tight line spacing metrics should succeed");
let metrics_spacious = calculate_font_metrics(None, 13.0, 1.5, 1.0, 1.0)
.expect("spacious line spacing metrics should succeed");
let ratio = metrics_spacious.cell_height / metrics_tight.cell_height;
assert!((ratio - 1.5).abs() < 0.01);
}
#[test]
fn test_char_spacing_affects_cell_width() {
let metrics_normal = calculate_font_metrics(None, 13.0, 1.0, 1.0, 1.0)
.expect("normal char spacing metrics should succeed");
let metrics_wide = calculate_font_metrics(None, 13.0, 1.0, 1.5, 1.0)
.expect("wide char spacing metrics should succeed");
let ratio = metrics_wide.cell_width / metrics_normal.cell_width;
assert!((ratio - 1.5).abs() < 0.01);
}
#[test]
fn test_scale_factor_affects_metrics() {
let metrics_1x = calculate_font_metrics(None, 13.0, 1.0, 1.0, 1.0)
.expect("1x scale factor metrics should succeed");
let metrics_2x = calculate_font_metrics(None, 13.0, 1.0, 1.0, 2.0)
.expect("2x scale factor metrics should succeed");
let width_ratio = metrics_2x.cell_width / metrics_1x.cell_width;
let height_ratio = metrics_2x.cell_height / metrics_1x.cell_height;
assert!((width_ratio - 2.0).abs() < 0.01);
assert!((height_ratio - 2.0).abs() < 0.01);
}
}