bold/italic support
This commit is contained in:
@@ -353,6 +353,10 @@ impl Keybindings {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
/// Font family name to use. The terminal will look for Regular, Bold, Italic, and BoldItalic
|
||||||
|
/// variants of this font. If not specified, falls back to system monospace fonts.
|
||||||
|
/// Example: "JetBrainsMono Nerd Font" or "0xProto Nerd Font"
|
||||||
|
pub font_family: Option<String>,
|
||||||
/// Font size in points.
|
/// Font size in points.
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
/// Position of the tab bar: "top", "bottom", or "hidden".
|
/// Position of the tab bar: "top", "bottom", or "hidden".
|
||||||
@@ -382,6 +386,7 @@ pub struct Config {
|
|||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
font_family: None,
|
||||||
font_size: 16.0,
|
font_size: 16.0,
|
||||||
tab_bar_position: TabBarPosition::Top,
|
tab_bar_position: TabBarPosition::Top,
|
||||||
background_opacity: 1.0,
|
background_opacity: 1.0,
|
||||||
|
|||||||
+492
-95
@@ -107,9 +107,46 @@ struct GlyphInfo {
|
|||||||
struct ShapingContext {
|
struct ShapingContext {
|
||||||
face: rustybuzz::Face<'static>,
|
face: rustybuzz::Face<'static>,
|
||||||
/// OpenType features to enable during shaping (liga, calt, etc.)
|
/// OpenType features to enable during shaping (liga, calt, etc.)
|
||||||
|
/// Note: This field is kept for potential future use when we need to modify
|
||||||
|
/// features per-context. Currently shaping_features on Renderer is used instead.
|
||||||
|
#[allow(dead_code)]
|
||||||
features: Vec<rustybuzz::Feature>,
|
features: Vec<rustybuzz::Feature>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Font style variant indices.
|
||||||
|
/// These map to the indices in font_variants array.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(usize)]
|
||||||
|
pub enum FontStyle {
|
||||||
|
Regular = 0,
|
||||||
|
Bold = 1,
|
||||||
|
Italic = 2,
|
||||||
|
BoldItalic = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontStyle {
|
||||||
|
/// Get the font style from bold and italic flags.
|
||||||
|
pub fn from_flags(bold: bool, italic: bool) -> Self {
|
||||||
|
match (bold, italic) {
|
||||||
|
(false, false) => FontStyle::Regular,
|
||||||
|
(true, false) => FontStyle::Bold,
|
||||||
|
(false, true) => FontStyle::Italic,
|
||||||
|
(true, true) => FontStyle::BoldItalic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A font variant with its data and parsed references.
|
||||||
|
struct FontVariant {
|
||||||
|
/// Owned font data (kept alive for the lifetime of the font references).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
data: Box<[u8]>,
|
||||||
|
/// ab_glyph font reference for rasterization.
|
||||||
|
font: FontRef<'static>,
|
||||||
|
/// rustybuzz face for text shaping.
|
||||||
|
face: rustybuzz::Face<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Result of shaping a text sequence.
|
/// Result of shaping a text sequence.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct ShapedGlyphs {
|
struct ShapedGlyphs {
|
||||||
@@ -256,6 +293,10 @@ pub struct Renderer {
|
|||||||
font_data: Box<[u8]>,
|
font_data: Box<[u8]>,
|
||||||
/// Primary font for rasterization (borrows font_data)
|
/// Primary font for rasterization (borrows font_data)
|
||||||
primary_font: FontRef<'static>,
|
primary_font: FontRef<'static>,
|
||||||
|
/// Font style variants: [Regular, Bold, Italic, BoldItalic]
|
||||||
|
/// Each entry is Option because some variants may not be available.
|
||||||
|
/// Index 0 (Regular) is always Some (same as primary_font's data).
|
||||||
|
font_variants: [Option<FontVariant>; 4],
|
||||||
/// Fallback fonts with their owned data
|
/// Fallback fonts with their owned data
|
||||||
fallback_fonts: Vec<(Box<[u8]>, FontRef<'static>)>,
|
fallback_fonts: Vec<(Box<[u8]>, FontRef<'static>)>,
|
||||||
/// Fontconfig handle for dynamic font discovery (lazy initialized)
|
/// Fontconfig handle for dynamic font discovery (lazy initialized)
|
||||||
@@ -263,9 +304,13 @@ pub struct Renderer {
|
|||||||
/// Set of font paths we've already tried (to avoid reloading)
|
/// Set of font paths we've already tried (to avoid reloading)
|
||||||
tried_font_paths: HashSet<PathBuf>,
|
tried_font_paths: HashSet<PathBuf>,
|
||||||
shaping_ctx: ShapingContext,
|
shaping_ctx: ShapingContext,
|
||||||
|
/// OpenType features for shaping (shared across all font variants)
|
||||||
|
shaping_features: Vec<rustybuzz::Feature>,
|
||||||
char_cache: HashMap<char, GlyphInfo>, // cache char -> rendered glyph
|
char_cache: HashMap<char, GlyphInfo>, // cache char -> rendered glyph
|
||||||
ligature_cache: HashMap<String, ShapedGlyphs>, // cache multi-char -> shaped glyphs
|
ligature_cache: HashMap<String, ShapedGlyphs>, // cache multi-char -> shaped glyphs
|
||||||
glyph_cache: HashMap<(usize, u16), GlyphInfo>, // keyed by (font_index, glyph ID)
|
/// Glyph cache keyed by (font_style, font_index, glyph_id)
|
||||||
|
/// font_style is FontStyle as usize, font_index is 0 for primary, 1+ for fallbacks
|
||||||
|
glyph_cache: HashMap<(usize, usize, u16), GlyphInfo>,
|
||||||
/// Cache for text run sprites (Kitty-style texture healing).
|
/// Cache for text run sprites (Kitty-style texture healing).
|
||||||
/// Keyed by the text string of the run. Value contains UV coords for each cell.
|
/// Keyed by the text string of the run. Value contains UV coords for each cell.
|
||||||
text_run_cache: HashMap<String, TextRunSprites>,
|
text_run_cache: HashMap<String, TextRunSprites>,
|
||||||
@@ -387,6 +432,191 @@ fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// FONT LOADING HELPER FUNCTIONS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Try to load a font file and create both ab_glyph and rustybuzz handles.
|
||||||
|
/// Returns None if the file doesn't exist or can't be parsed.
|
||||||
|
fn load_font_variant(path: &std::path::Path) -> Option<FontVariant> {
|
||||||
|
let data = std::fs::read(path).ok()?.into_boxed_slice();
|
||||||
|
|
||||||
|
// Parse with ab_glyph
|
||||||
|
let font: FontRef<'static> = {
|
||||||
|
let font = FontRef::try_from_slice(&data).ok()?;
|
||||||
|
// SAFETY: We keep data alive in the FontVariant struct
|
||||||
|
unsafe { std::mem::transmute(font) }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse with rustybuzz
|
||||||
|
let face: rustybuzz::Face<'static> = {
|
||||||
|
let face = rustybuzz::Face::from_slice(&data, 0)?;
|
||||||
|
// SAFETY: We keep data alive in the FontVariant struct
|
||||||
|
unsafe { std::mem::transmute(face) }
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(FontVariant { data, font, face })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find font files for a font family using fontconfig.
|
||||||
|
/// Returns paths for (regular, bold, italic, bold_italic).
|
||||||
|
/// Any variant that can't be found will be None.
|
||||||
|
fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
|
||||||
|
use fontconfig_sys as fcsys;
|
||||||
|
use fcsys::*;
|
||||||
|
use fcsys::constants::{FC_FAMILY, FC_WEIGHT, FC_SLANT, FC_FILE};
|
||||||
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
let mut results: [Option<PathBuf>; 4] = [None, None, None, None];
|
||||||
|
|
||||||
|
// Style queries: (weight, slant) pairs for each variant
|
||||||
|
// FC_WEIGHT_REGULAR = 80, FC_WEIGHT_BOLD = 200
|
||||||
|
// FC_SLANT_ROMAN = 0, FC_SLANT_ITALIC = 100
|
||||||
|
let styles: [(i32, i32); 4] = [
|
||||||
|
(80, 0), // Regular
|
||||||
|
(200, 0), // Bold
|
||||||
|
(80, 100), // Italic
|
||||||
|
(200, 100), // BoldItalic
|
||||||
|
];
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let family_cstr = match CString::new(family) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return results,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (idx, (weight, slant)) in styles.iter().enumerate() {
|
||||||
|
let pat = FcPatternCreate();
|
||||||
|
if pat.is_null() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set family name
|
||||||
|
FcPatternAddString(pat, FC_FAMILY.as_ptr() as *const i8, family_cstr.as_ptr() as *const u8);
|
||||||
|
// Set weight
|
||||||
|
FcPatternAddInteger(pat, FC_WEIGHT.as_ptr() as *const i8, *weight);
|
||||||
|
// Set slant
|
||||||
|
FcPatternAddInteger(pat, FC_SLANT.as_ptr() as *const i8, *slant);
|
||||||
|
|
||||||
|
FcConfigSubstitute(std::ptr::null_mut(), pat, FcMatchPattern);
|
||||||
|
FcDefaultSubstitute(pat);
|
||||||
|
|
||||||
|
let mut result: FcResult = FcResultMatch;
|
||||||
|
let matched = FcFontMatch(std::ptr::null_mut(), pat, &mut result);
|
||||||
|
|
||||||
|
if result == FcResultMatch && !matched.is_null() {
|
||||||
|
let mut file_ptr: *mut u8 = std::ptr::null_mut();
|
||||||
|
if FcPatternGetString(matched, FC_FILE.as_ptr() as *const i8, 0, &mut file_ptr) == FcResultMatch {
|
||||||
|
if !file_ptr.is_null() {
|
||||||
|
let path_cstr = std::ffi::CStr::from_ptr(file_ptr as *const i8);
|
||||||
|
if let Ok(path_str) = path_cstr.to_str() {
|
||||||
|
results[idx] = Some(PathBuf::from(path_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FcPatternDestroy(matched);
|
||||||
|
}
|
||||||
|
|
||||||
|
FcPatternDestroy(pat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load font variants for a font family.
|
||||||
|
/// Returns array of font variants, with index 0 being the regular font.
|
||||||
|
/// Falls back to hardcoded paths if fontconfig fails.
|
||||||
|
fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'static>, [Option<FontVariant>; 4]) {
|
||||||
|
// Try to use fontconfig to find the font family
|
||||||
|
if let Some(family) = font_family {
|
||||||
|
let paths = find_font_family_variants(family);
|
||||||
|
log::info!("Font family '{}' resolved to:", family);
|
||||||
|
for (i, path) in paths.iter().enumerate() {
|
||||||
|
let style = match i {
|
||||||
|
0 => "Regular",
|
||||||
|
1 => "Bold",
|
||||||
|
2 => "Italic",
|
||||||
|
3 => "BoldItalic",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
if let Some(p) = path {
|
||||||
|
log::info!(" {}: {:?}", style, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the regular font (required)
|
||||||
|
if let Some(regular_path) = &paths[0] {
|
||||||
|
if let Some(regular) = load_font_variant(regular_path) {
|
||||||
|
let primary_font = regular.font.clone();
|
||||||
|
let font_data = regular.data.clone();
|
||||||
|
|
||||||
|
// Load other variants
|
||||||
|
let variants: [Option<FontVariant>; 4] = [
|
||||||
|
Some(regular),
|
||||||
|
paths[1].as_ref().and_then(|p| load_font_variant(p)),
|
||||||
|
paths[2].as_ref().and_then(|p| load_font_variant(p)),
|
||||||
|
paths[3].as_ref().and_then(|p| load_font_variant(p)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (font_data, primary_font, variants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::warn!("Failed to load font family '{}', falling back to defaults", family);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try hardcoded paths
|
||||||
|
let fallback_fonts = [
|
||||||
|
("/usr/share/fonts/TTF/0xProtoNerdFont-Regular.ttf",
|
||||||
|
"/usr/share/fonts/TTF/0xProtoNerdFont-Bold.ttf",
|
||||||
|
"/usr/share/fonts/TTF/0xProtoNerdFont-Italic.ttf",
|
||||||
|
"/usr/share/fonts/TTF/0xProtoNerdFont-BoldItalic.ttf"),
|
||||||
|
("/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Bold.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Italic.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-BoldItalic.ttf"),
|
||||||
|
("/usr/share/fonts/TTF/JetBrainsMono-Regular.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMono-Bold.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMono-Italic.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMono-BoldItalic.ttf"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (regular, bold, italic, bold_italic) in fallback_fonts {
|
||||||
|
let regular_path = std::path::Path::new(regular);
|
||||||
|
if let Some(regular_variant) = load_font_variant(regular_path) {
|
||||||
|
let primary_font = regular_variant.font.clone();
|
||||||
|
let font_data = regular_variant.data.clone();
|
||||||
|
|
||||||
|
let variants: [Option<FontVariant>; 4] = [
|
||||||
|
Some(regular_variant),
|
||||||
|
load_font_variant(std::path::Path::new(bold)),
|
||||||
|
load_font_variant(std::path::Path::new(italic)),
|
||||||
|
load_font_variant(std::path::Path::new(bold_italic)),
|
||||||
|
];
|
||||||
|
|
||||||
|
log::info!("Loaded font from fallback paths:");
|
||||||
|
log::info!(" Regular: {}", regular);
|
||||||
|
if variants[1].is_some() { log::info!(" Bold: {}", bold); }
|
||||||
|
if variants[2].is_some() { log::info!(" Italic: {}", italic); }
|
||||||
|
if variants[3].is_some() { log::info!(" BoldItalic: {}", bold_italic); }
|
||||||
|
|
||||||
|
return (font_data, primary_font, variants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: try NotoSansMono
|
||||||
|
let noto_regular = std::path::Path::new("/usr/share/fonts/noto/NotoSansMono-Regular.ttf");
|
||||||
|
if let Some(regular_variant) = load_font_variant(noto_regular) {
|
||||||
|
let primary_font = regular_variant.font.clone();
|
||||||
|
let font_data = regular_variant.data.clone();
|
||||||
|
let variants: [Option<FontVariant>; 4] = [Some(regular_variant), None, None, None];
|
||||||
|
log::info!("Loaded NotoSansMono as fallback");
|
||||||
|
return (font_data, primary_font, variants);
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("Failed to load any monospace font");
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// BOX DRAWING HELPER TYPES
|
// BOX DRAWING HELPER TYPES
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -848,41 +1078,17 @@ impl Renderer {
|
|||||||
};
|
};
|
||||||
surface.configure(&device, &surface_config);
|
surface.configure(&device, &surface_config);
|
||||||
|
|
||||||
// Load primary font
|
// Load primary font and font variants (regular, bold, italic, bold-italic)
|
||||||
let font_data: Box<[u8]> = std::fs::read("/usr/share/fonts/TTF/0xProtoNerdFont-Regular.ttf")
|
let (font_data, primary_font, font_variants) = load_font_family(config.font_family.as_deref());
|
||||||
.or_else(|_| std::fs::read("/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf"))
|
|
||||||
.or_else(|_| std::fs::read("/usr/share/fonts/TTF/JetBrainsMono-Regular.ttf"))
|
|
||||||
.or_else(|_| std::fs::read("/usr/share/fonts/noto/NotoSansMono-Regular.ttf"))
|
|
||||||
.expect("Failed to load any monospace font")
|
|
||||||
.into_boxed_slice();
|
|
||||||
|
|
||||||
// Parse font with ab_glyph for rasterization
|
|
||||||
// SAFETY: We transmute to 'static because font_data lives as long as Renderer.
|
|
||||||
let primary_font: FontRef<'static> = {
|
|
||||||
let font = FontRef::try_from_slice(&font_data)
|
|
||||||
.expect("Failed to parse font with ab_glyph");
|
|
||||||
unsafe { std::mem::transmute(font) }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fontconfig will be initialized lazily on first fallback font lookup
|
// Fontconfig will be initialized lazily on first fallback font lookup
|
||||||
|
|
||||||
// Start with empty fallback fonts - will be loaded on-demand via fontconfig
|
// Start with empty fallback fonts - will be loaded on-demand via fontconfig
|
||||||
let fallback_fonts: Vec<(Box<[u8]>, FontRef<'static>)> = Vec::new();
|
let fallback_fonts: Vec<(Box<[u8]>, FontRef<'static>)> = Vec::new();
|
||||||
let tried_font_paths: HashSet<PathBuf> = HashSet::new();
|
let tried_font_paths: HashSet<PathBuf> = HashSet::new();
|
||||||
|
|
||||||
// Create rustybuzz Face for text shaping (ligatures).
|
|
||||||
// SAFETY: We transmute to 'static because font_data lives as long as Renderer.
|
|
||||||
// The Face only borrows the data, so this is safe as long as we don't drop font_data
|
|
||||||
// before dropping the Face, which is guaranteed by struct drop order.
|
|
||||||
let face: rustybuzz::Face<'static> = {
|
|
||||||
let face = rustybuzz::Face::from_slice(&font_data, 0)
|
|
||||||
.expect("Failed to parse font for shaping");
|
|
||||||
unsafe { std::mem::transmute(face) }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enable OpenType features for ligatures and contextual alternates
|
// Enable OpenType features for ligatures and contextual alternates
|
||||||
// These are the standard features used by coding fonts like Fira Code, JetBrains Mono, etc.
|
// These are the standard features used by coding fonts like Fira Code, JetBrains Mono, etc.
|
||||||
let features = vec![
|
let shaping_features = vec![
|
||||||
// Standard ligatures (fi, fl, etc.)
|
// Standard ligatures (fi, fl, etc.)
|
||||||
rustybuzz::Feature::new(Tag::from_bytes(b"liga"), 1, ..),
|
rustybuzz::Feature::new(Tag::from_bytes(b"liga"), 1, ..),
|
||||||
// Contextual alternates (programming ligatures like ->, =>, etc.)
|
// Contextual alternates (programming ligatures like ->, =>, etc.)
|
||||||
@@ -890,7 +1096,17 @@ impl Renderer {
|
|||||||
// Discretionary ligatures (optional ligatures)
|
// Discretionary ligatures (optional ligatures)
|
||||||
rustybuzz::Feature::new(Tag::from_bytes(b"dlig"), 1, ..),
|
rustybuzz::Feature::new(Tag::from_bytes(b"dlig"), 1, ..),
|
||||||
];
|
];
|
||||||
let shaping_ctx = ShapingContext { face: face.clone(), features };
|
|
||||||
|
// Create shaping context using the regular font variant's face
|
||||||
|
// The face is borrowed from font_variants[0], which is always Some
|
||||||
|
let shaping_ctx = {
|
||||||
|
let regular_variant = font_variants[0].as_ref()
|
||||||
|
.expect("Regular font variant should always be present");
|
||||||
|
ShapingContext {
|
||||||
|
face: regular_variant.face.clone(),
|
||||||
|
features: shaping_features.clone(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Calculate cell dimensions from font metrics using ab_glyph
|
// Calculate cell dimensions from font metrics using ab_glyph
|
||||||
//
|
//
|
||||||
@@ -909,42 +1125,6 @@ impl Renderer {
|
|||||||
// ab_glyph's height() = ascent - descent (where descent is negative)
|
// ab_glyph's height() = ascent - descent (where descent is negative)
|
||||||
let cell_height = scaled_font.height().round();
|
let cell_height = scaled_font.height().round();
|
||||||
|
|
||||||
// DEBUG: Compare ab_glyph and rustybuzz advance calculations
|
|
||||||
{
|
|
||||||
let units_per_em = face.units_per_em();
|
|
||||||
let height_unscaled = primary_font.height_unscaled();
|
|
||||||
|
|
||||||
// ab_glyph uses: scale / height_unscaled as the scale factor for h_advance
|
|
||||||
let ab_scale_factor = font_size / height_unscaled;
|
|
||||||
// We were using: scale / units_per_em (WRONG!)
|
|
||||||
let our_wrong_scale = font_size / units_per_em as f32;
|
|
||||||
|
|
||||||
// Get 'M' glyph advance from rustybuzz's underlying ttf_parser
|
|
||||||
let _m_glyph_rb = face.glyph_index('M').map(|g| g.0).unwrap_or(0);
|
|
||||||
// Let's shape a single 'M' and see what advance we get
|
|
||||||
let mut buffer = rustybuzz::UnicodeBuffer::new();
|
|
||||||
buffer.push_str("M");
|
|
||||||
let glyph_buffer = rustybuzz::shape(&face, &[], buffer);
|
|
||||||
let positions = glyph_buffer.glyph_positions();
|
|
||||||
if !positions.is_empty() {
|
|
||||||
let rb_advance_font_units = positions[0].x_advance;
|
|
||||||
let rb_advance_px_wrong = rb_advance_font_units as f32 * our_wrong_scale;
|
|
||||||
let rb_advance_px_correct = rb_advance_font_units as f32 * ab_scale_factor;
|
|
||||||
let ab_advance_raw = scaled_font.h_advance(m_glyph_id);
|
|
||||||
eprintln!("DEBUG font metrics:");
|
|
||||||
eprintln!(" font_size = {} px", font_size);
|
|
||||||
eprintln!(" units_per_em = {}", units_per_em);
|
|
||||||
eprintln!(" height_unscaled = {} (ascent - descent)", height_unscaled);
|
|
||||||
eprintln!(" ab_glyph scale factor = font_size / height_unscaled = {}", ab_scale_factor);
|
|
||||||
eprintln!(" our WRONG scale factor = font_size / units_per_em = {}", our_wrong_scale);
|
|
||||||
eprintln!(" ab_glyph 'M' h_advance (raw) = {}", ab_advance_raw);
|
|
||||||
eprintln!(" ab_glyph 'M' h_advance (rounded) = {}", cell_width);
|
|
||||||
eprintln!(" rustybuzz 'M' x_advance (font units) = {}", rb_advance_font_units);
|
|
||||||
eprintln!(" rustybuzz 'M' x_advance (WRONG pixels) = {}", rb_advance_px_wrong);
|
|
||||||
eprintln!(" rustybuzz 'M' x_advance (CORRECT pixels) = {}", rb_advance_px_correct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the correct scale factor for converting font units to pixels.
|
// Calculate the correct scale factor for converting font units to pixels.
|
||||||
// This matches ab_glyph's calculation: scale / height_unscaled
|
// This matches ab_glyph's calculation: scale / height_unscaled
|
||||||
// where height_unscaled = ascent - descent (the font's natural line height).
|
// where height_unscaled = ascent - descent (the font's natural line height).
|
||||||
@@ -1297,10 +1477,12 @@ impl Renderer {
|
|||||||
atlas_dirty: false,
|
atlas_dirty: false,
|
||||||
font_data,
|
font_data,
|
||||||
primary_font,
|
primary_font,
|
||||||
|
font_variants,
|
||||||
fallback_fonts,
|
fallback_fonts,
|
||||||
fontconfig: OnceCell::new(),
|
fontconfig: OnceCell::new(),
|
||||||
tried_font_paths,
|
tried_font_paths,
|
||||||
shaping_ctx,
|
shaping_ctx,
|
||||||
|
shaping_features,
|
||||||
char_cache: HashMap::new(),
|
char_cache: HashMap::new(),
|
||||||
ligature_cache: HashMap::new(),
|
ligature_cache: HashMap::new(),
|
||||||
glyph_cache: HashMap::new(),
|
glyph_cache: HashMap::new(),
|
||||||
@@ -3010,8 +3192,12 @@ impl Renderer {
|
|||||||
|
|
||||||
/// Get or rasterize a glyph by its glyph ID from the primary font.
|
/// Get or rasterize a glyph by its glyph ID from the primary font.
|
||||||
/// Used for ligatures where we have the glyph ID from rustybuzz.
|
/// Used for ligatures where we have the glyph ID from rustybuzz.
|
||||||
|
/// Note: Kept for potential fallback use. Use get_glyph_by_id_with_style for styled text.
|
||||||
|
#[allow(dead_code)]
|
||||||
fn get_glyph_by_id(&mut self, glyph_id: u16) -> GlyphInfo {
|
fn get_glyph_by_id(&mut self, glyph_id: u16) -> GlyphInfo {
|
||||||
let cache_key = (0usize, glyph_id); // font index 0 = primary font
|
// Cache key: (font_style, font_index, glyph_id)
|
||||||
|
// For now, we use Regular style (0) and primary font index (0)
|
||||||
|
let cache_key = (FontStyle::Regular as usize, 0usize, glyph_id);
|
||||||
if let Some(info) = self.glyph_cache.get(&cache_key) {
|
if let Some(info) = self.glyph_cache.get(&cache_key) {
|
||||||
return *info;
|
return *info;
|
||||||
}
|
}
|
||||||
@@ -3093,8 +3279,107 @@ impl Renderer {
|
|||||||
info
|
info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get or rasterize a glyph by its glyph ID from a specific font variant.
|
||||||
|
/// Uses bold/italic font if available, otherwise falls back to regular.
|
||||||
|
fn get_glyph_by_id_with_style(&mut self, glyph_id: u16, style: FontStyle) -> GlyphInfo {
|
||||||
|
// Cache key: (font_style, font_index, glyph_id)
|
||||||
|
// font_index 0 = primary/regular font
|
||||||
|
let cache_key = (style as usize, 0usize, glyph_id);
|
||||||
|
if let Some(info) = self.glyph_cache.get(&cache_key) {
|
||||||
|
return *info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the font for the requested style
|
||||||
|
let font = if style == FontStyle::Regular {
|
||||||
|
self.primary_font.clone()
|
||||||
|
} else if let Some(ref variant) = self.font_variants[style as usize] {
|
||||||
|
variant.font.clone()
|
||||||
|
} else {
|
||||||
|
// Fall back to regular font if variant not available
|
||||||
|
self.primary_font.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rasterize the glyph by ID using ab_glyph
|
||||||
|
let ab_glyph_id = GlyphId(glyph_id);
|
||||||
|
let raster_result = self.rasterize_glyph_ab(&font, ab_glyph_id);
|
||||||
|
|
||||||
|
let Some((glyph_width, glyph_height, bitmap, offset_x, offset_y)) = raster_result else {
|
||||||
|
// Empty glyph (e.g., space)
|
||||||
|
let info = GlyphInfo {
|
||||||
|
uv: [0.0, 0.0, 0.0, 0.0],
|
||||||
|
offset: [0.0, 0.0],
|
||||||
|
size: [0.0, 0.0],
|
||||||
|
};
|
||||||
|
self.glyph_cache.insert(cache_key, info);
|
||||||
|
return info;
|
||||||
|
};
|
||||||
|
|
||||||
|
if bitmap.is_empty() || glyph_width == 0 || glyph_height == 0 {
|
||||||
|
// Empty glyph (e.g., space)
|
||||||
|
let info = GlyphInfo {
|
||||||
|
uv: [0.0, 0.0, 0.0, 0.0],
|
||||||
|
offset: [0.0, 0.0],
|
||||||
|
size: [0.0, 0.0],
|
||||||
|
};
|
||||||
|
self.glyph_cache.insert(cache_key, info);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we need to move to next row
|
||||||
|
if self.atlas_cursor_x + glyph_width > ATLAS_SIZE {
|
||||||
|
self.atlas_cursor_x = 0;
|
||||||
|
self.atlas_cursor_y += self.atlas_row_height + 1;
|
||||||
|
self.atlas_row_height = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if atlas is full
|
||||||
|
if self.atlas_cursor_y + glyph_height > ATLAS_SIZE {
|
||||||
|
log::warn!("Glyph atlas is full!");
|
||||||
|
let info = GlyphInfo {
|
||||||
|
uv: [0.0, 0.0, 0.0, 0.0],
|
||||||
|
offset: [0.0, 0.0],
|
||||||
|
size: [0.0, 0.0],
|
||||||
|
};
|
||||||
|
self.glyph_cache.insert(cache_key, info);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy bitmap to atlas
|
||||||
|
for y in 0..glyph_height as usize {
|
||||||
|
for x in 0..glyph_width as usize {
|
||||||
|
let src_idx = y * glyph_width as usize + x;
|
||||||
|
let dst_x = self.atlas_cursor_x + x as u32;
|
||||||
|
let dst_y = self.atlas_cursor_y + y as u32;
|
||||||
|
let dst_idx = (dst_y * ATLAS_SIZE + dst_x) as usize;
|
||||||
|
self.atlas_data[dst_idx] = bitmap[src_idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.atlas_dirty = true;
|
||||||
|
|
||||||
|
// Calculate UV coordinates
|
||||||
|
let uv_x = self.atlas_cursor_x as f32 / ATLAS_SIZE as f32;
|
||||||
|
let uv_y = self.atlas_cursor_y as f32 / ATLAS_SIZE as f32;
|
||||||
|
let uv_w = glyph_width as f32 / ATLAS_SIZE as f32;
|
||||||
|
let uv_h = glyph_height as f32 / ATLAS_SIZE as f32;
|
||||||
|
|
||||||
|
let info = GlyphInfo {
|
||||||
|
uv: [uv_x, uv_y, uv_w, uv_h],
|
||||||
|
offset: [offset_x, offset_y],
|
||||||
|
size: [glyph_width as f32, glyph_height as f32],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update atlas cursor
|
||||||
|
self.atlas_cursor_x += glyph_width + 1;
|
||||||
|
self.atlas_row_height = self.atlas_row_height.max(glyph_height);
|
||||||
|
|
||||||
|
self.glyph_cache.insert(cache_key, info);
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
/// Shape a text string using HarfBuzz/rustybuzz.
|
/// Shape a text string using HarfBuzz/rustybuzz.
|
||||||
/// Returns glyph IDs with advances and offsets for texture healing.
|
/// Returns glyph IDs with advances and offsets for texture healing.
|
||||||
|
/// Note: Kept for potential fallback use. Use shape_text_with_style for styled text.
|
||||||
|
#[allow(dead_code)]
|
||||||
fn shape_text(&mut self, text: &str) -> ShapedGlyphs {
|
fn shape_text(&mut self, text: &str) -> ShapedGlyphs {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
if let Some(cached) = self.ligature_cache.get(text) {
|
if let Some(cached) = self.ligature_cache.get(text) {
|
||||||
@@ -3134,6 +3419,53 @@ impl Renderer {
|
|||||||
shaped
|
shaped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shape a text string using HarfBuzz/rustybuzz with a specific font style.
|
||||||
|
/// Uses the bold/italic font variant if available, otherwise falls back to regular.
|
||||||
|
fn shape_text_with_style(&mut self, text: &str, style: FontStyle) -> ShapedGlyphs {
|
||||||
|
// For now, we'll create a cache key that includes style
|
||||||
|
// TODO: Could optimize by having separate caches per style
|
||||||
|
let cache_key = format!("{}\x00{}", style as usize, text);
|
||||||
|
if let Some(cached) = self.ligature_cache.get(&cache_key) {
|
||||||
|
return cached.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = UnicodeBuffer::new();
|
||||||
|
buffer.push_str(text);
|
||||||
|
|
||||||
|
// Get the face for the requested style, falling back to regular if not available
|
||||||
|
let face = if style == FontStyle::Regular {
|
||||||
|
&self.shaping_ctx.face
|
||||||
|
} else if let Some(ref variant) = self.font_variants[style as usize] {
|
||||||
|
&variant.face
|
||||||
|
} else {
|
||||||
|
// Fall back to regular font
|
||||||
|
&self.shaping_ctx.face
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shape with OpenType features enabled (liga, calt, dlig)
|
||||||
|
let glyph_buffer = rustybuzz::shape(face, &self.shaping_features, buffer);
|
||||||
|
let glyph_infos = glyph_buffer.glyph_infos();
|
||||||
|
let glyph_positions = glyph_buffer.glyph_positions();
|
||||||
|
|
||||||
|
let glyphs: Vec<(u16, f32, f32, f32, u32)> = glyph_infos
|
||||||
|
.iter()
|
||||||
|
.zip(glyph_positions.iter())
|
||||||
|
.map(|(info, pos)| {
|
||||||
|
let glyph_id = info.glyph_id as u16;
|
||||||
|
// Note: We don't pre-rasterize here; that happens in render_glyphs_to_canvas_with_style
|
||||||
|
// Convert from font units to pixels using the correct scale factor.
|
||||||
|
let x_advance = pos.x_advance as f32 * self.font_units_to_px;
|
||||||
|
let x_offset = pos.x_offset as f32 * self.font_units_to_px;
|
||||||
|
let y_offset = pos.y_offset as f32 * self.font_units_to_px;
|
||||||
|
(glyph_id, x_advance, x_offset, y_offset, info.cluster)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let shaped = ShapedGlyphs { glyphs };
|
||||||
|
self.ligature_cache.insert(cache_key, shaped.clone());
|
||||||
|
shaped
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// KITTY-STYLE TEXTURE HEALING: CANVAS-BASED TEXT RUN RENDERING
|
// KITTY-STYLE TEXTURE HEALING: CANVAS-BASED TEXT RUN RENDERING
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -3170,11 +3502,13 @@ impl Renderer {
|
|||||||
|
|
||||||
/// Render shaped glyphs into the canvas buffer using HarfBuzz positions.
|
/// Render shaped glyphs into the canvas buffer using HarfBuzz positions.
|
||||||
/// This is the core of Kitty's texture healing approach.
|
/// This is the core of Kitty's texture healing approach.
|
||||||
|
/// Note: Kept for potential fallback use. Use render_glyphs_to_canvas_with_style for styled text.
|
||||||
///
|
///
|
||||||
/// Arguments:
|
/// Arguments:
|
||||||
/// - `shaped`: The shaped glyph data from HarfBuzz
|
/// - `shaped`: The shaped glyph data from HarfBuzz
|
||||||
/// - `num_cells`: Number of cells this text run spans
|
/// - `num_cells`: Number of cells this text run spans
|
||||||
/// - `baseline_offset`: Offset from top of cell to baseline (typically cell_height * 0.8)
|
/// - `baseline_offset`: Offset from top of cell to baseline (typically cell_height * 0.8)
|
||||||
|
#[allow(dead_code)]
|
||||||
fn render_glyphs_to_canvas(&mut self, shaped: &ShapedGlyphs, num_cells: usize, baseline_offset: f32) {
|
fn render_glyphs_to_canvas(&mut self, shaped: &ShapedGlyphs, num_cells: usize, baseline_offset: f32) {
|
||||||
let canvas_width = (self.cell_width as usize) * num_cells;
|
let canvas_width = (self.cell_width as usize) * num_cells;
|
||||||
let canvas_height = self.cell_height as usize;
|
let canvas_height = self.cell_height as usize;
|
||||||
@@ -3247,6 +3581,79 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render shaped glyphs into the canvas buffer using a specific font style.
|
||||||
|
/// This version uses the appropriate font variant for bold/italic text.
|
||||||
|
fn render_glyphs_to_canvas_with_style(&mut self, shaped: &ShapedGlyphs, num_cells: usize, baseline_offset: f32, style: FontStyle) {
|
||||||
|
let canvas_width = (self.cell_width as usize) * num_cells;
|
||||||
|
let canvas_height = self.cell_height as usize;
|
||||||
|
|
||||||
|
// Track cursor position in canvas coordinates
|
||||||
|
let mut cursor_x: f32 = 0.0;
|
||||||
|
|
||||||
|
for &(glyph_id, x_advance, x_offset, y_offset, _cluster) in &shaped.glyphs {
|
||||||
|
// Get the rasterized glyph bitmap using the correct font variant
|
||||||
|
let glyph = self.get_glyph_by_id_with_style(glyph_id, style);
|
||||||
|
|
||||||
|
if glyph.size[0] <= 0.0 || glyph.size[1] <= 0.0 {
|
||||||
|
// Empty glyph (e.g., space) - just advance cursor
|
||||||
|
cursor_x += x_advance;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate glyph position in canvas:
|
||||||
|
// - cursor_x: accumulated from x_advance
|
||||||
|
// - x_offset: HarfBuzz adjustment for texture healing
|
||||||
|
// - glyph.offset[0]: left bearing from the font
|
||||||
|
let glyph_x = cursor_x + x_offset + glyph.offset[0];
|
||||||
|
|
||||||
|
// Y position: baseline_offset is from top to baseline
|
||||||
|
// glyph.offset[1] is distance from baseline to glyph bottom (negative = below baseline)
|
||||||
|
// For canvas coords (y=0 at top), we need: baseline_offset - glyph_top
|
||||||
|
// glyph_top = glyph.offset[1] + glyph.size[1] (since offset is to bottom)
|
||||||
|
let glyph_y = baseline_offset + y_offset - glyph.offset[1] - glyph.size[1];
|
||||||
|
|
||||||
|
// Round to integers for pixel placement
|
||||||
|
let dest_x = glyph_x.round() as i32;
|
||||||
|
let dest_y = glyph_y.round() as i32;
|
||||||
|
|
||||||
|
// Get glyph bitmap from atlas
|
||||||
|
// The glyph.uv gives us the location in the atlas
|
||||||
|
let atlas_x = (glyph.uv[0] * ATLAS_SIZE as f32) as u32;
|
||||||
|
let atlas_y = (glyph.uv[1] * ATLAS_SIZE as f32) as u32;
|
||||||
|
let glyph_w = glyph.size[0] as u32;
|
||||||
|
let glyph_h = glyph.size[1] as u32;
|
||||||
|
|
||||||
|
// Copy glyph bitmap from atlas to canvas, with bounds checking
|
||||||
|
for gy in 0..glyph_h {
|
||||||
|
let canvas_y = dest_y + gy as i32;
|
||||||
|
if canvas_y < 0 || canvas_y >= canvas_height as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for gx in 0..glyph_w {
|
||||||
|
let canvas_x = dest_x + gx as i32;
|
||||||
|
if canvas_x < 0 || canvas_x >= canvas_width as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source in atlas
|
||||||
|
let src_idx = ((atlas_y + gy) * ATLAS_SIZE + (atlas_x + gx)) as usize;
|
||||||
|
// Destination in canvas
|
||||||
|
let dst_idx = (canvas_y as usize) * canvas_width + (canvas_x as usize);
|
||||||
|
|
||||||
|
// Blend: use max for overlapping glyphs (simple compositing)
|
||||||
|
let src_alpha = self.atlas_data[src_idx];
|
||||||
|
let dst_alpha = self.canvas_buffer[dst_idx];
|
||||||
|
self.canvas_buffer[dst_idx] = src_alpha.max(dst_alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance cursor by glyph's advance width.
|
||||||
|
// Round the advance to ensure it aligns with cell boundaries for monospace fonts.
|
||||||
|
cursor_x += x_advance.round();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract a single cell from the canvas and upload it to the atlas.
|
/// Extract a single cell from the canvas and upload it to the atlas.
|
||||||
/// Returns the GlyphInfo for this cell's sprite.
|
/// Returns the GlyphInfo for this cell's sprite.
|
||||||
fn extract_cell_from_canvas(&mut self, cell_index: usize, num_cells: usize) -> [f32; 4] {
|
fn extract_cell_from_canvas(&mut self, cell_index: usize, num_cells: usize) -> [f32; 4] {
|
||||||
@@ -3295,26 +3702,15 @@ impl Renderer {
|
|||||||
|
|
||||||
/// Render a text run using Kitty's canvas-based approach for texture healing.
|
/// Render a text run using Kitty's canvas-based approach for texture healing.
|
||||||
/// Returns TextRunSprites containing UV coordinates for each cell.
|
/// Returns TextRunSprites containing UV coordinates for each cell.
|
||||||
fn render_text_run(&mut self, text: &str, num_cells: usize) -> TextRunSprites {
|
fn render_text_run(&mut self, text: &str, num_cells: usize, style: FontStyle) -> TextRunSprites {
|
||||||
// Check cache first
|
// Check cache first - include font style in cache key
|
||||||
if let Some(cached) = self.text_run_cache.get(text) {
|
let cache_key = format!("{}\x00{}", style as usize, text);
|
||||||
|
if let Some(cached) = self.text_run_cache.get(&cache_key) {
|
||||||
return cached.clone();
|
return cached.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shape the text
|
// Shape the text using the appropriate font variant
|
||||||
let shaped = self.shape_text(text);
|
let shaped = self.shape_text_with_style(text, style);
|
||||||
|
|
||||||
// DEBUG: Log shaping results
|
|
||||||
if num_cells >= 2 {
|
|
||||||
let total_advance: f32 = shaped.glyphs.iter().map(|g| g.1).sum();
|
|
||||||
let expected_width = self.cell_width * num_cells as f32;
|
|
||||||
eprintln!("DEBUG shape_text: text='{}' num_cells={} num_glyphs={} total_advance={:.2} expected_width={:.2} cell_width={:.2}",
|
|
||||||
text, num_cells, shaped.glyphs.len(), total_advance, expected_width, self.cell_width);
|
|
||||||
for (i, &(glyph_id, x_advance, x_offset, _y_offset, cluster)) in shaped.glyphs.iter().enumerate() {
|
|
||||||
eprintln!(" glyph[{}]: id={} x_advance={:.2} x_offset={:.2} cluster={}",
|
|
||||||
i, glyph_id, x_advance, x_offset, cluster);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure canvas is big enough
|
// Ensure canvas is big enough
|
||||||
self.ensure_canvas_size(num_cells);
|
self.ensure_canvas_size(num_cells);
|
||||||
@@ -3323,8 +3719,8 @@ impl Renderer {
|
|||||||
// Calculate baseline offset (typically ~80% down from top of cell)
|
// Calculate baseline offset (typically ~80% down from top of cell)
|
||||||
let baseline_offset = self.cell_height * 0.8;
|
let baseline_offset = self.cell_height * 0.8;
|
||||||
|
|
||||||
// Render all glyphs into the canvas
|
// Render all glyphs into the canvas using the appropriate font variant
|
||||||
self.render_glyphs_to_canvas(&shaped, num_cells, baseline_offset);
|
self.render_glyphs_to_canvas_with_style(&shaped, num_cells, baseline_offset, style);
|
||||||
|
|
||||||
// Extract each cell from the canvas
|
// Extract each cell from the canvas
|
||||||
let mut cells = Vec::with_capacity(num_cells);
|
let mut cells = Vec::with_capacity(num_cells);
|
||||||
@@ -3334,7 +3730,7 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let sprites = TextRunSprites { cells };
|
let sprites = TextRunSprites { cells };
|
||||||
self.text_run_cache.insert(text.to_string(), sprites.clone());
|
self.text_run_cache.insert(cache_key, sprites.clone());
|
||||||
sprites
|
sprites
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3491,7 +3887,10 @@ impl Renderer {
|
|||||||
(color_to_rgba(&cell.fg_color, true), color_to_rgba(&cell.bg_color, false))
|
(color_to_rgba(&cell.fg_color, true), color_to_rgba(&cell.bg_color, false))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Collect a run of cells with the same fg/bg colors
|
// Track font style for bold/italic rendering
|
||||||
|
let base_style = FontStyle::from_flags(cell.bold, cell.italic);
|
||||||
|
|
||||||
|
// Collect a run of cells with the same fg/bg colors AND font style
|
||||||
let mut run_text = String::new();
|
let mut run_text = String::new();
|
||||||
let mut run_cells: Vec<(usize, char, bool)> = Vec::new(); // (col, char, is_box_drawing)
|
let mut run_cells: Vec<(usize, char, bool)> = Vec::new(); // (col, char, is_box_drawing)
|
||||||
|
|
||||||
@@ -3505,8 +3904,11 @@ impl Renderer {
|
|||||||
(color_to_rgba(&run_cell.fg_color, true), color_to_rgba(&run_cell.bg_color, false))
|
(color_to_rgba(&run_cell.fg_color, true), color_to_rgba(&run_cell.bg_color, false))
|
||||||
};
|
};
|
||||||
|
|
||||||
if cell_fg != base_fg || cell_bg != base_bg {
|
// Check if font style (bold/italic) matches
|
||||||
break; // Different colors, end this run
|
let cell_style = FontStyle::from_flags(run_cell.bold, run_cell.italic);
|
||||||
|
|
||||||
|
if cell_fg != base_fg || cell_bg != base_bg || cell_style != base_style {
|
||||||
|
break; // Different colors or font style, end this run
|
||||||
}
|
}
|
||||||
|
|
||||||
let c = run_cell.character;
|
let c = run_cell.character;
|
||||||
@@ -3587,12 +3989,7 @@ impl Renderer {
|
|||||||
// Render the text run using canvas approach if there's text to shape
|
// Render the text run using canvas approach if there's text to shape
|
||||||
let sprites = if !shape_text.is_empty() && shape_indices.len() == run_cells.len() {
|
let sprites = if !shape_text.is_empty() && shape_indices.len() == run_cells.len() {
|
||||||
// All cells are shapeable text - use canvas rendering
|
// All cells are shapeable text - use canvas rendering
|
||||||
// DEBUG: Log what we're shaping
|
Some(self.render_text_run(&run_text, run_cells.len(), base_style))
|
||||||
if run_cells.len() >= 2 {
|
|
||||||
eprintln!("DEBUG render_text_run: text='{}' num_cells={} shape_text='{}'",
|
|
||||||
run_text, run_cells.len(), shape_text);
|
|
||||||
}
|
|
||||||
Some(self.render_text_run(&run_text, run_cells.len()))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user