font family
This commit is contained in:
+1
-13
@@ -55,19 +55,7 @@ fn drag_window(ui: &mut egui::Ui) {
|
||||
}
|
||||
|
||||
pub fn fonts(config: &Config) -> Result<egui::FontDefinitions> {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
if let Some(path) = &config.font_path {
|
||||
let data = std::fs::read(path)?;
|
||||
fonts
|
||||
.font_data
|
||||
.insert("terminal".into(), egui::FontData::from_owned(data).into());
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&egui::FontFamily::Monospace)
|
||||
.unwrap()
|
||||
.insert(0, "terminal".into());
|
||||
}
|
||||
Ok(fonts)
|
||||
crate::fonts::load(config)
|
||||
}
|
||||
|
||||
impl App {
|
||||
|
||||
@@ -14,6 +14,11 @@ pub struct Config {
|
||||
pub working_directory: Option<PathBuf>,
|
||||
pub font_size: f32,
|
||||
pub font_path: Option<PathBuf>,
|
||||
pub font_family: Option<String>,
|
||||
pub font_bold_path: Option<PathBuf>,
|
||||
pub font_italic_path: Option<PathBuf>,
|
||||
pub font_bold_italic_path: Option<PathBuf>,
|
||||
pub font_fallbacks: Vec<String>,
|
||||
pub scrollback: usize,
|
||||
pub window_width: f32,
|
||||
pub window_height: f32,
|
||||
@@ -99,6 +104,11 @@ impl Default for Config {
|
||||
working_directory: None,
|
||||
font_size: 16.0,
|
||||
font_path: None,
|
||||
font_family: None,
|
||||
font_bold_path: None,
|
||||
font_italic_path: None,
|
||||
font_bold_italic_path: None,
|
||||
font_fallbacks: Vec::new(),
|
||||
scrollback: 10000,
|
||||
window_width: 1100.0,
|
||||
window_height: 720.0,
|
||||
@@ -153,6 +163,18 @@ impl Config {
|
||||
|
||||
pub fn parse(text: &str) -> Result<Self> {
|
||||
let config: Self = toml::from_str(text)?;
|
||||
ensure!(
|
||||
config.font_path.is_none() || config.font_family.is_none(),
|
||||
"set either font_family or font_path, not both"
|
||||
);
|
||||
ensure!(
|
||||
config
|
||||
.font_family
|
||||
.iter()
|
||||
.chain(&config.font_fallbacks)
|
||||
.all(|name| !name.trim().is_empty()),
|
||||
"font family names must not be empty"
|
||||
);
|
||||
ensure!(
|
||||
!config.shell.is_empty() && !config.shell[0].is_empty(),
|
||||
"shell must contain an executable"
|
||||
@@ -250,6 +272,9 @@ mod tests {
|
||||
assert_eq!(example.keys, Config::default().keys);
|
||||
for text in [
|
||||
"font_size = nan",
|
||||
"font_family = ''",
|
||||
"font_fallbacks = [' ']",
|
||||
"font_family = 'Test'\nfont_path = '/font.ttf'",
|
||||
"shell = []",
|
||||
"background = '#xx0000'",
|
||||
"font_sze = 16",
|
||||
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
use crate::config::Config;
|
||||
use anyhow::{Context, Result, ensure};
|
||||
use egui::{FontData, FontDefinitions, FontFamily};
|
||||
use fontdb::{Database, Family, ID, Query, Style, Weight};
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
pub fn family(bold: bool, italic: bool) -> FontFamily {
|
||||
match (bold, italic) {
|
||||
(false, false) => FontFamily::Monospace,
|
||||
(true, false) => FontFamily::Name("terminal-bold".into()),
|
||||
(false, true) => FontFamily::Name("terminal-italic".into()),
|
||||
(true, true) => FontFamily::Name("terminal-bold-italic".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn query(db: &Database, name: &str, bold: bool, italic: bool) -> Option<ID> {
|
||||
db.query(&Query {
|
||||
families: &[Family::Name(name)],
|
||||
weight: if bold { Weight::BOLD } else { Weight::NORMAL },
|
||||
style: if italic { Style::Italic } else { Style::Normal },
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn load_file(db: &mut Database, path: &Path) -> Result<ID> {
|
||||
let bytes =
|
||||
std::fs::read(path).with_context(|| format!("Cannot read font {}", path.display()))?;
|
||||
db.load_font_source(fontdb::Source::Binary(std::sync::Arc::new(bytes)))
|
||||
.first()
|
||||
.copied()
|
||||
.with_context(|| format!("No font faces in {}", path.display()))
|
||||
}
|
||||
|
||||
fn register(
|
||||
db: &Database,
|
||||
fonts: &mut FontDefinitions,
|
||||
loaded: &mut HashMap<ID, String>,
|
||||
id: ID,
|
||||
) -> Result<String> {
|
||||
if let Some(name) = loaded.get(&id) {
|
||||
return Ok(name.clone());
|
||||
}
|
||||
let face = db.face(id).context("Font face disappeared")?;
|
||||
let data = db
|
||||
.with_face_data(id, |bytes, index| {
|
||||
// Validate with the renderer's parser too, so a bad font becomes a
|
||||
// reload error rather than a panic inside egui on the next frame.
|
||||
ab_glyph::FontRef::try_from_slice_and_index(bytes, index)
|
||||
.with_context(|| format!("Unsupported font: {}", face.post_script_name))?;
|
||||
let mut data = FontData::from_owned(bytes.to_vec());
|
||||
data.index = index;
|
||||
Ok::<_, anyhow::Error>(data)
|
||||
})
|
||||
.context("Cannot read font face data")??;
|
||||
let name = format!("terminal-face-{}", loaded.len());
|
||||
fonts.font_data.insert(name.clone(), data.into());
|
||||
loaded.insert(id, name.clone());
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
pub fn load(config: &Config) -> Result<FontDefinitions> {
|
||||
let mut db = Database::new();
|
||||
// Preserve the built-in default when no family is configured. System
|
||||
// fonts also supply optional Nerd Font symbols and named fallbacks.
|
||||
db.load_system_fonts();
|
||||
load_from_database(config, db)
|
||||
}
|
||||
|
||||
fn load_from_database(config: &Config, mut db: Database) -> Result<FontDefinitions> {
|
||||
let mut primary = config.font_family.clone();
|
||||
let regular = if let Some(path) = &config.font_path {
|
||||
let id = load_file(&mut db, path)?;
|
||||
primary = db
|
||||
.face(id)
|
||||
.and_then(|face| face.families.first())
|
||||
.map(|f| f.0.clone());
|
||||
// Inspect sibling metadata, not filename suffixes: foundries use
|
||||
// different style names, and collections contain multiple faces.
|
||||
let parent = path
|
||||
.parent()
|
||||
.filter(|p| !p.as_os_str().is_empty())
|
||||
.unwrap_or(Path::new("."));
|
||||
for entry in std::fs::read_dir(parent)?.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()).is_some_and(|e| {
|
||||
["ttf", "otf", "ttc", "otc"]
|
||||
.iter()
|
||||
.any(|ext| e.eq_ignore_ascii_case(ext))
|
||||
}) {
|
||||
let _ = db.load_font_file(path);
|
||||
}
|
||||
}
|
||||
Some(id)
|
||||
} else if let Some(name) = &primary {
|
||||
Some(
|
||||
query(&db, name, false, false)
|
||||
.with_context(|| format!("Font family not found: {name}"))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
for name in &config.font_fallbacks {
|
||||
ensure!(
|
||||
query(&db, name, false, false).is_some(),
|
||||
"Fallback font family not found: {name}"
|
||||
);
|
||||
}
|
||||
let mut fallbacks = config.font_fallbacks.clone();
|
||||
for name in ["Symbols Nerd Font Mono", "Symbols Nerd Font"] {
|
||||
if query(&db, name, false, false).is_some() && !fallbacks.iter().any(|f| f == name) {
|
||||
fallbacks.push(name.into());
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut fonts = FontDefinitions::default();
|
||||
let builtin = fonts.families[&FontFamily::Monospace].clone();
|
||||
let mut loaded = HashMap::new();
|
||||
for (bold, italic, path) in [
|
||||
(false, false, None),
|
||||
(true, false, config.font_bold_path.as_deref()),
|
||||
(false, true, config.font_italic_path.as_deref()),
|
||||
(true, true, config.font_bold_italic_path.as_deref()),
|
||||
] {
|
||||
let id = if let Some(path) = path {
|
||||
Some(load_file(&mut db, path)?)
|
||||
} else if !bold && !italic {
|
||||
regular
|
||||
} else {
|
||||
primary
|
||||
.as_ref()
|
||||
.and_then(|name| query(&db, name, bold, italic))
|
||||
.or(regular)
|
||||
};
|
||||
let mut chain = Vec::new();
|
||||
if let Some(id) = id {
|
||||
chain.push(register(&db, &mut fonts, &mut loaded, id)?);
|
||||
} else {
|
||||
// A symbols-only fallback must not set the terminal's line metrics.
|
||||
chain.push(builtin[0].clone());
|
||||
}
|
||||
// A style face may lack symbols present in its regular face.
|
||||
if let Some(id) = regular {
|
||||
let name = register(&db, &mut fonts, &mut loaded, id)?;
|
||||
if !chain.contains(&name) {
|
||||
chain.push(name);
|
||||
}
|
||||
}
|
||||
for name in &fallbacks {
|
||||
for id in [
|
||||
query(&db, name, bold, italic),
|
||||
query(&db, name, false, false),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
let name = register(&db, &mut fonts, &mut loaded, id)?;
|
||||
if !chain.contains(&name) {
|
||||
chain.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
chain.extend(builtin.iter().cloned());
|
||||
fonts.families.insert(family(bold, italic), chain);
|
||||
}
|
||||
Ok(fonts)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn database() -> Database {
|
||||
let mut db = Database::new();
|
||||
let fonts = FontDefinitions::default();
|
||||
let data = &fonts.font_data[&fonts.families[&FontFamily::Monospace][0]];
|
||||
db.load_font_data(data.font.to_vec());
|
||||
let template = db.faces().next().unwrap().clone();
|
||||
for (bold, italic) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let mut face = template.clone();
|
||||
face.families[0].0 = "Test Family".into();
|
||||
face.weight = if bold { Weight::BOLD } else { Weight::NORMAL };
|
||||
face.style = if italic { Style::Italic } else { Style::Normal };
|
||||
db.push_face_info(face);
|
||||
}
|
||||
db
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_styles_and_keeps_regular_as_fallback() {
|
||||
let config = Config {
|
||||
font_family: Some("Test Family".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let db = database();
|
||||
let mut ids = std::collections::HashSet::new();
|
||||
for (bold, italic) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let id = query(&db, "Test Family", bold, italic).unwrap();
|
||||
assert!(ids.insert(id), "styles must select distinct faces");
|
||||
let face = db.face(id).unwrap();
|
||||
assert_eq!(face.weight == Weight::BOLD, bold);
|
||||
assert_eq!(face.style == Style::Italic, italic);
|
||||
}
|
||||
let fonts = load_from_database(&config, db).unwrap();
|
||||
let regular = &fonts.families[&family(false, false)][0];
|
||||
for (bold, italic) in [(true, false), (false, true), (true, true)] {
|
||||
let chain = &fonts.families[&family(bold, italic)];
|
||||
assert_ne!(&chain[0], regular);
|
||||
assert_eq!(&chain[1], regular);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_families_and_accepts_regular_only_fallback() {
|
||||
let mut config = Config {
|
||||
font_family: Some("Not Installed".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(load_from_database(&config, database()).is_err());
|
||||
config.font_family = None;
|
||||
config.font_fallbacks = vec!["Not Installed".into()];
|
||||
assert!(load_from_database(&config, database()).is_err());
|
||||
let db = database();
|
||||
let name = db.faces().next().unwrap().families[0].0.clone();
|
||||
config.font_fallbacks = vec![name];
|
||||
let fonts = load_from_database(&config, db).unwrap();
|
||||
assert_eq!(
|
||||
fonts.families[&family(false, false)],
|
||||
fonts.families[&family(true, true)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires SLERM_TEST_FONT pointing to a Nerd Font regular face with sibling styles"]
|
||||
fn installed_nerd_font_family_and_sibling_styles() {
|
||||
use ab_glyph::Font;
|
||||
let path = std::env::var_os("SLERM_TEST_FONT").expect("set SLERM_TEST_FONT");
|
||||
let config = Config {
|
||||
font_path: Some(path.into()),
|
||||
..Default::default()
|
||||
};
|
||||
let fonts = load(&config).unwrap();
|
||||
let mut faces = std::collections::HashSet::new();
|
||||
for (bold, italic) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let name = &fonts.families[&family(bold, italic)][0];
|
||||
assert!(faces.insert(name.clone()));
|
||||
let data = &fonts.font_data[name];
|
||||
let face = ab_glyph::FontRef::try_from_slice_and_index(&data.font, data.index).unwrap();
|
||||
for c in ['M', '\u{f121}', '\u{e0a0}', '\u{f0001}'] {
|
||||
assert_ne!(face.glyph_id(c).0, 0, "missing {c:?} in {name}");
|
||||
}
|
||||
}
|
||||
let mut db = Database::new();
|
||||
let id = load_file(&mut db, config.font_path.as_ref().unwrap()).unwrap();
|
||||
let name = db.face(id).unwrap().families[0].0.clone();
|
||||
eprintln!("Verified installed family: {name}");
|
||||
let named = load(&Config {
|
||||
font_family: Some(name),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
for (bold, italic) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let a = &fonts.font_data[&fonts.families[&family(bold, italic)][0]];
|
||||
let b = &named.font_data[&named.families[&family(bold, italic)][0]];
|
||||
assert_eq!(a.font, b.font);
|
||||
assert_eq!(a.index, b.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod app;
|
||||
mod config;
|
||||
mod fonts;
|
||||
mod input;
|
||||
mod layout;
|
||||
mod pane;
|
||||
|
||||
+181
-11
@@ -7,6 +7,7 @@ use libghostty_vt::{
|
||||
alloc::{Allocator, Bytes},
|
||||
kitty::graphics::{DecodePng, DecodedImage, ImageFormat, Layer, PlacementIterator},
|
||||
render::{CellIterator, Colors, CursorVisualStyle, Dirty, RowIterator},
|
||||
screen::CellWide,
|
||||
style::{RgbColor, Underline},
|
||||
};
|
||||
use std::{
|
||||
@@ -128,13 +129,51 @@ struct CachedRow {
|
||||
}
|
||||
|
||||
fn layout_glyph(painter: &Painter, text: String, font: &FontId, height: f32) -> Arc<egui::Galley> {
|
||||
layout_styled_glyph(painter, text, font, height, false)
|
||||
}
|
||||
|
||||
fn layout_styled_glyph(
|
||||
painter: &Painter,
|
||||
text: String,
|
||||
font: &FontId,
|
||||
height: f32,
|
||||
italics: bool,
|
||||
) -> Arc<egui::Galley> {
|
||||
let mut job =
|
||||
egui::text::LayoutJob::simple(text, font.clone(), Color32::PLACEHOLDER, f32::INFINITY);
|
||||
job.sections[0].format.line_height = Some(height);
|
||||
job.sections[0].format.valign = egui::Align::Center;
|
||||
job.sections[0].format.italics = italics;
|
||||
painter.layout_job(job)
|
||||
}
|
||||
|
||||
fn nerd_icon(text: &str) -> bool {
|
||||
// Includes the supplementary private-use plane used by Material Design.
|
||||
text.chars()
|
||||
.next()
|
||||
.is_some_and(|c| matches!(c as u32, 0xe000..=0xf8ff | 0xf0000..=0xffffd))
|
||||
}
|
||||
|
||||
fn icon_shape(galley: Arc<egui::Galley>, rect: Rect, color: Color32) -> egui::Shape {
|
||||
// Use ink bounds: patched fonts can have bearings or outlines that extend
|
||||
// well beyond their nominal advance. Fit uniformly and center in the cell
|
||||
// allocation so icons cannot cover neighboring text or rows.
|
||||
let bounds = galley.mesh_bounds;
|
||||
let mut shape = egui::epaint::TextShape::new(egui::Pos2::ZERO, galley, color);
|
||||
if bounds.is_positive() {
|
||||
let scale = (rect.width() / bounds.width())
|
||||
.min(rect.height() / bounds.height())
|
||||
.min(1.0);
|
||||
shape.transform(egui::emath::TSTransform {
|
||||
scaling: scale,
|
||||
translation: rect.center().to_vec2() - bounds.center().to_vec2() * scale,
|
||||
});
|
||||
} else {
|
||||
shape.pos = rect.min;
|
||||
}
|
||||
shape.into()
|
||||
}
|
||||
|
||||
pub struct Renderer {
|
||||
state: RenderState<'static>,
|
||||
rows: RowIterator<'static>,
|
||||
@@ -142,8 +181,9 @@ pub struct Renderer {
|
||||
placements: PlacementIterator<'static>,
|
||||
textures: HashMap<u32, (u64, TextureHandle)>,
|
||||
row_cache: Vec<CachedRow>,
|
||||
glyphs: HashMap<String, Arc<egui::Galley>>,
|
||||
glyphs: HashMap<(String, bool, bool), Arc<egui::Galley>>,
|
||||
font_marker: Option<Arc<egui::Galley>>,
|
||||
synthetic_styles: [(bool, bool); 4],
|
||||
cached_colors: Option<Colors>,
|
||||
cached_cell: egui::Vec2,
|
||||
cached_cols: u16,
|
||||
@@ -162,6 +202,7 @@ impl Renderer {
|
||||
row_cache: Vec::new(),
|
||||
glyphs: HashMap::new(),
|
||||
font_marker: None,
|
||||
synthetic_styles: [(false, false); 4],
|
||||
cached_colors: None,
|
||||
cached_cell: egui::Vec2::ZERO,
|
||||
cached_cols: 0,
|
||||
@@ -183,18 +224,46 @@ impl Renderer {
|
||||
// Keep one egui galley alive in its cache each frame. A new identity means
|
||||
// fonts/DPI/the atlas were reset, invalidating every cached glyph's UVs.
|
||||
let marker = layout_glyph(&painter, "M".into(), font, cell.y);
|
||||
if self
|
||||
let fonts_changed = self
|
||||
.font_marker
|
||||
.as_ref()
|
||||
.is_none_or(|old| !Arc::ptr_eq(old, &marker))
|
||||
|| self.cached_cell != cell
|
||||
|| self.glyphs.len() > 4096
|
||||
{
|
||||
|| self.glyphs.len() > 4096;
|
||||
if fonts_changed {
|
||||
self.row_cache.clear();
|
||||
self.glyphs.clear();
|
||||
}
|
||||
self.font_marker = Some(marker);
|
||||
self.cached_cell = cell;
|
||||
let style_fonts = painter.ctx().fonts(|fonts| {
|
||||
[(false, false), (true, false), (false, true), (true, true)].map(|(bold, italic)| {
|
||||
let family = crate::fonts::family(bold, italic);
|
||||
if font.family == egui::FontFamily::Monospace
|
||||
&& fonts.definitions().families.contains_key(&family)
|
||||
{
|
||||
FontId::new(font.size, family)
|
||||
} else {
|
||||
font.clone()
|
||||
}
|
||||
})
|
||||
});
|
||||
if fonts_changed {
|
||||
self.synthetic_styles = painter.ctx().fonts(|fonts| {
|
||||
let first_face = |index: usize| {
|
||||
fonts.definitions().families[&style_fonts[index].family]
|
||||
.first()
|
||||
.map(|name| &fonts.definitions().font_data[name])
|
||||
};
|
||||
std::array::from_fn::<_, 4, _>(|index| {
|
||||
(
|
||||
index & 1 != 0 && first_face(index) == first_face(index & !1),
|
||||
index & 2 != 0 && first_face(index) == first_face(index & !2),
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
let synthetic = self.synthetic_styles;
|
||||
let snapshot = self.state.update(term)?;
|
||||
let colors = snapshot.colors()?;
|
||||
let cols = snapshot.cols()?;
|
||||
@@ -246,27 +315,52 @@ impl Renderer {
|
||||
if style.faint {
|
||||
fg = fg.gamma_multiply(0.6);
|
||||
}
|
||||
if !style.invisible && c.graphemes_len()? > 0 {
|
||||
let wide = c.raw_cell()?.wide()?;
|
||||
if !style.invisible
|
||||
&& !matches!(wide, CellWide::SpacerHead | CellWide::SpacerTail)
|
||||
&& c.graphemes_len()? > 0
|
||||
{
|
||||
c.graphemes_utf8(&mut text)?;
|
||||
// Placeholder cells are reserved by the Kitty protocol, not printable text.
|
||||
if let Some(separator) = powerline_shape(&text, cell_rect, fg) {
|
||||
foregrounds.push(separator);
|
||||
} else if text != " " && !text.starts_with('\u{10eeee}') {
|
||||
let galley = if let Some(galley) = self.glyphs.get(&text) {
|
||||
let index = usize::from(style.bold) + 2 * usize::from(style.italic);
|
||||
let icon = nerd_icon(&text);
|
||||
let key = (text.clone(), style.bold, style.italic);
|
||||
let galley = if let Some(galley) = self.glyphs.get(&key) {
|
||||
galley.clone()
|
||||
} else {
|
||||
let galley = layout_glyph(&painter, text.clone(), font, cell.y);
|
||||
self.glyphs.insert(text.clone(), galley.clone());
|
||||
let font = &style_fonts[index];
|
||||
let galley = layout_styled_glyph(
|
||||
&painter,
|
||||
text.clone(),
|
||||
font,
|
||||
cell.y,
|
||||
synthetic[index].1 && !icon,
|
||||
);
|
||||
self.glyphs.insert(key, galley.clone());
|
||||
galley
|
||||
};
|
||||
foregrounds.push(egui::Shape::galley(pos, galley.clone(), fg));
|
||||
if style.bold {
|
||||
// Keep style emphasis for regular-only fonts; real style
|
||||
// faces are drawn once. Never distort symbol outlines.
|
||||
if synthetic[index].0 && !icon {
|
||||
foregrounds.push(egui::Shape::galley(
|
||||
pos + vec2(0.45, 0.0),
|
||||
galley,
|
||||
galley.clone(),
|
||||
fg,
|
||||
));
|
||||
}
|
||||
foregrounds.push(if icon {
|
||||
let width = if wide == CellWide::Wide { 2.0 } else { 1.0 };
|
||||
icon_shape(
|
||||
galley,
|
||||
Rect::from_min_size(pos, vec2(cell.x * width, cell.y)),
|
||||
fg,
|
||||
)
|
||||
} else {
|
||||
egui::Shape::galley(pos, galley, fg)
|
||||
});
|
||||
}
|
||||
}
|
||||
for (enabled, height) in [
|
||||
@@ -451,6 +545,82 @@ fn paint_images(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sgr_styles_use_distinct_cached_faces_without_double_drawing() {
|
||||
let ctx = egui::Context::default();
|
||||
let config = crate::config::Config {
|
||||
font_path: std::env::var_os("SLERM_TEST_FONT").map(Into::into),
|
||||
..Default::default()
|
||||
};
|
||||
ctx.set_fonts(crate::fonts::load(&config).unwrap());
|
||||
let mut term = crate::pane::terminal(&config).unwrap();
|
||||
term.resize(20, 2, 8, 16).unwrap();
|
||||
term.vt_write(b"M\x1b[1mM\x1b[22;3mM\x1b[1mM\x1b[0mM");
|
||||
let mut renderer = Renderer::new().unwrap();
|
||||
let rect = Rect::from_min_size(egui::Pos2::ZERO, vec2(200.0, 100.0));
|
||||
let font = FontId::monospace(16.0);
|
||||
frame(&ctx, &mut renderer, &term, rect, &font);
|
||||
assert_eq!(renderer.glyphs.len(), 4);
|
||||
assert_eq!(
|
||||
renderer.row_cache[0].foregrounds.len(),
|
||||
if config.font_path.is_some() { 5 } else { 7 }
|
||||
);
|
||||
for (bold, italic) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let galley = &renderer.glyphs[&("M".into(), bold, italic)];
|
||||
assert_eq!(
|
||||
galley.job.sections[0].format.font_id.family,
|
||||
crate::fonts::family(bold, italic)
|
||||
);
|
||||
}
|
||||
let before = renderer.glyphs[&("M".into(), true, true)].clone();
|
||||
let mut fonts = crate::fonts::load(&config).unwrap();
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&crate::fonts::family(true, true))
|
||||
.unwrap()
|
||||
.reverse();
|
||||
ctx.set_fonts(fonts);
|
||||
frame(&ctx, &mut renderer, &term, rect, &font);
|
||||
assert!(!Arc::ptr_eq(
|
||||
&before,
|
||||
&renderer.glyphs[&("M".into(), true, true)]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icons_fit_ink_bounds_at_different_scales_and_cell_widths() {
|
||||
let config = crate::config::Config {
|
||||
font_path: std::env::var_os("SLERM_TEST_FONT").map(Into::into),
|
||||
..Default::default()
|
||||
};
|
||||
let fonts = crate::fonts::load(&config).unwrap();
|
||||
for scale in [1.0, 1.5, 2.0] {
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_fonts(fonts.clone());
|
||||
ctx.set_pixels_per_point(scale);
|
||||
let _ = ctx.run(egui::RawInput::default(), |ctx| {
|
||||
let painter = ctx.layer_painter(egui::LayerId::background());
|
||||
for text in ["\u{e0a0}", "\u{f121}", "\u{f0001}"] {
|
||||
assert!(nerd_icon(text));
|
||||
for width in [4.0, 9.0, 18.0] {
|
||||
let rect = Rect::from_min_size(pos2(23.0, 41.0), vec2(width, 18.0));
|
||||
let galley =
|
||||
layout_glyph(&painter, text.into(), &FontId::monospace(20.0), 18.0);
|
||||
let shape = icon_shape(galley, rect, Color32::WHITE);
|
||||
let bounds = shape.visual_bounding_rect();
|
||||
assert!(
|
||||
rect.expand(0.001).contains_rect(bounds),
|
||||
"{text:?}: {bounds:?} outside {rect:?}"
|
||||
);
|
||||
assert!(bounds.center().distance(rect.center()) < 0.001);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
assert!(!nerd_icon("M"));
|
||||
assert!(!nerd_icon("\u{10eeee}"));
|
||||
}
|
||||
|
||||
fn frame(
|
||||
ctx: &egui::Context,
|
||||
renderer: &mut Renderer,
|
||||
|
||||
Reference in New Issue
Block a user