diff options
| author | srdusr <[email protected]> | 2025-02-15 14:56:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-02-15 14:56:00 +0200 |
| commit | 0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch) | |
| tree | 7672d0af277664f457c6c9462925c0005fe35dcf /crates/wayland/src/decoration/font.rs | |
| parent | 413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff) | |
| download | srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.tar.gz srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.zip | |
Checkpoint: preserve all uncommitted rust-rewrite worktree work
Safety commit before reconciling this worktree with main, which has
diverged with its own separate fixes today. Nothing here is reviewed
or curated yet - this exists purely so none of this work can be lost
to a git operation, disk issue, or worktree cleanup while that
reconciliation happens.
Diffstat (limited to 'crates/wayland/src/decoration/font.rs')
| -rw-r--r-- | crates/wayland/src/decoration/font.rs | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/crates/wayland/src/decoration/font.rs b/crates/wayland/src/decoration/font.rs new file mode 100644 index 0000000..27df347 --- /dev/null +++ b/crates/wayland/src/decoration/font.rs @@ -0,0 +1,146 @@ +//! Locating a system monospace font and blitting its rasterized glyphs into +//! a titlebar's own pixel buffer - everything `render_titlebar`'s title- +//! text pass needs, kept separate from the titlebar layout/button logic +//! that actually calls it. + +use super::color::rgb_to_bgra; +use fontdue::{Font, FontSettings}; +use std::sync::OnceLock; + +pub(crate) const FONT_PIXELS: f32 = 13.0; +pub(crate) const TEXT_LEFT_PADDING: f32 = 8.0; + +/// Common monospace font file locations on Linux desktops. Not a full +/// fontconfig query (no new system dependency for something this small) -- +/// if none of these resolve, titlebars fall back to solid-color-only, same +/// as before text rendering existed. +pub(crate) fn find_system_font() -> Option<Font> { + static FONT: OnceLock<Option<Font>> = OnceLock::new(); + FONT.get_or_init(load_any_monospace_font).clone() +} + +fn load_any_monospace_font() -> Option<Font> { + let roots = ["/usr/share/fonts", "/usr/local/share/fonts"]; + let mut home_roots = Vec::new(); + if let Ok(home) = std::env::var("HOME") { + home_roots.push(format!("{home}/.local/share/fonts")); + home_roots.push(format!("{home}/.fonts")); + } + let all_roots = roots.iter().map(|s| s.to_string()).chain(home_roots); + + let mut best: Option<(std::path::PathBuf, u8)> = None; + for root in all_roots { + find_best_font(std::path::Path::new(&root), &mut best); + if matches!(best, Some((_, 0))) { + break; + } + } + let (path, _) = best?; + let bytes = std::fs::read(&path).ok()?; + match Font::from_bytes(bytes, FontSettings::default()) { + Ok(f) => { + log::info!("wayland titlebar font: {}", path.display()); + Some(f) + } + Err(e) => { + log::warn!("failed to parse font {}: {e}", path.display()); + None + } + } +} + +/// Ranks a font file by how suitable it is for titlebar text: `0` (best) is +/// a mono-named file with no weight/style marker at all (a plain +/// "Regular"), `1` is mono but italic/oblique/some other non-regular +/// weight, `2` is not mono-named at all. Lower is better. +/// +/// The style-marker list matters as much as the mono check: without it, a +/// directory listing that happens to turn up `...Mono...-Italic.ttf` +/// before any regular-weight mono file sorts no worse than one, and gets +/// picked and stuck with for the process's whole lifetime (`find_system_ +/// font`'s own `OnceLock`). Reported live: this exact case, `/usr/share/ +/// fonts/TTF/JetBrainsMonoNerdFontPropo-Italic.ttf` picked over every +/// regular-weight JetBrains Mono variant also installed on the same +/// system, purely because "mono" matched and nothing excluded italic -- +/// every titlebar rendered in italic instead of upright text. +fn font_rank(path: &std::path::Path) -> u8 { + let name = path.to_string_lossy().to_lowercase(); + let is_mono = name.contains("mono"); + let is_styled = + ["italic", "oblique", "bold", "light", "thin", "black", "medium", "semibold", "extrabold", "condensed"].iter().any(|s| name.contains(s)); + match (is_mono, is_styled) { + (true, false) => 0, + (true, true) => 1, + (false, _) => 2, + } +} + +/// Walks `dir` for the lowest-`font_rank` `.ttf`/`.otf` file, checking +/// every file rather than stopping at the first mono match - unlike rank +/// alone, "first found" says nothing about *style*, and the filesystem's +/// own directory-listing order is not something to trust for that. Only +/// stops early once a genuine rank-`0` (mono, unstyled) match is found, +/// since nothing could ever beat that. +fn find_best_font(dir: &std::path::Path, best: &mut Option<(std::path::PathBuf, u8)>) { + if matches!(best, Some((_, 0))) { + return; + } + let Ok(entries) = std::fs::read_dir(dir) else { return }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + find_best_font(&path, best); + if matches!(best, Some((_, 0))) { + return; + } + continue; + } + let is_font = path.extension().and_then(|e| e.to_str()).map(|e| e.eq_ignore_ascii_case("ttf") || e.eq_ignore_ascii_case("otf")).unwrap_or(false); + if !is_font { + continue; + } + let rank = font_rank(&path); + if best.as_ref().is_none_or(|(_, r)| rank < *r) { + *best = Some((path, rank)); + if rank == 0 { + return; + } + } + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn blit_glyph( + buf: &mut [u8], + width: usize, + height: usize, + glyph_x: i32, + glyph_y: i32, + metrics: &fontdue::Metrics, + coverage: &[u8], + background: (u8, u8, u8), + foreground: (u8, u8, u8), +) { + for row in 0..metrics.height { + let y = glyph_y + row as i32; + if y < 0 || y as usize >= height { + continue; + } + for col in 0..metrics.width { + let x = glyph_x + col as i32; + if x < 0 || x as usize >= width { + continue; + } + let cov = coverage[row * metrics.width + col] as f32 / 255.0; + if cov <= 0.0 { + continue; + } + let blend = |bg: u8, fg: u8| -> u8 { (bg as f32 * (1.0 - cov) + fg as f32 * cov).round() as u8 }; + let r = blend(background.0, foreground.0); + let g = blend(background.1, foreground.1); + let b = blend(background.2, foreground.2); + let idx = (y as usize * width + x as usize) * 4; + buf[idx..idx + 4].copy_from_slice(&rgb_to_bgra((r, g, b), 255)); + } + } +} |