srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/decoration/font.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-02-15 14:56:00 +0200
committersrdusr <[email protected]>2025-02-15 14:56:00 +0200
commit0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch)
tree7672d0af277664f457c6c9462925c0005fe35dcf /crates/wayland/src/decoration/font.rs
parent413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff)
downloadsrdwm-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.rs146
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));
+ }
+ }
+}