// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Backend-neutral helpers for the software renderer: rounded-rect //! path construction + system-font lookup. use tiny_skia::{ Path, PathBuilder }; use crate::types::Corners; /// Cubic bezier control-point factor for a quarter-circle approximation /// (`(4/3) * (sqrt(2) - 1) ≈ 0.5523`). const KAPPA: f32 = 0.5523_f32; /// Build a rounded rectangle path with independent per-corner radii /// using cubic bezier curves. Each corner is clamped against the /// inscribed-circle limit `min(width, height) / 2` before drawing, /// so callers can pass theme pill sentinels (e.g. `RADIUS = 100`) and /// still get a well-formed pill on a small rect. pub ( super ) fn build_rounded_rect( rect: tiny_skia::Rect, corners: Corners ) -> Option { let c = corners.clamp_to_size( rect.width(), rect.height() ); let tl = c.tl; let tr = c.tr; let br = c.br; let bl = c.bl; let x0 = rect.left(); let y0 = rect.top(); let x1 = rect.right(); let y1 = rect.bottom(); let mut pb = PathBuilder::new(); pb.move_to( x0 + tl, y0 ); pb.line_to( x1 - tr, y0 ); if tr > 0.0 { let kk = tr * KAPPA; pb.cubic_to( x1 - tr + kk, y0, x1, y0 + tr - kk, x1, y0 + tr ); } pb.line_to( x1, y1 - br ); if br > 0.0 { let kk = br * KAPPA; pb.cubic_to( x1, y1 - br + kk, x1 - br + kk, y1, x1 - br, y1 ); } pb.line_to( x0 + bl, y1 ); if bl > 0.0 { let kk = bl * KAPPA; pb.cubic_to( x0 + bl - kk, y1, x0, y1 - bl + kk, x0, y1 - bl ); } pb.line_to( x0, y0 + tl ); if tl > 0.0 { let kk = tl * KAPPA; pb.cubic_to( x0, y0 + tl - kk, x0 + tl - kk, y0, x0 + tl, y0 ); } pb.close(); pb.finish() } /// System-font search chain, ordered by preference. Shared by /// [`find_font`] (which panics when none match) and /// [`find_font_opt`] (which returns `None` — used by tests that /// want to skip gracefully on images without the usual fonts /// installed). const SYSTEM_FONT_CANDIDATES: &[&str] = &[ // Debian `fonts-sora` — the canonical path `ltk-theme-default` // depends on. Listed first so Sora wins as the default font // whenever the package is installed. "/usr/share/fonts/opentype/sora/Sora-Regular.otf", "/usr/share/fonts/truetype/sora/Sora-Regular.ttf", "/usr/share/fonts/sora/Sora-Regular.ttf", "/usr/share/fonts/TTF/Sora-Regular.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", "/usr/share/fonts/liberation/LiberationSans-Regular.ttf", "/usr/share/fonts/truetype/freefont/FreeSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/dejavu/DejaVuSans.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf", ]; /// Resolve the first system font available from /// [`SYSTEM_FONT_CANDIDATES`], or `None` if none exist. Used by /// tests; runtime code uses [`find_font`]. pub ( crate ) fn find_font_opt() -> Option { SYSTEM_FONT_CANDIDATES.iter() .find( |p| std::path::Path::new( p ).exists() ) .copied() .map( str::to_string ) } /// Load the bytes of a default system font. Tries the candidate chain /// via [`find_font_opt`]; falls back to the embedded /// [`crate::theme::fallback::FALLBACK_FONT`] (Sora Regular, ~50 KB, /// OFL 1.1) when nothing matches or the file cannot be read. Always /// returns usable bytes so canvas construction never panics on a /// system without the expected fonts. pub ( super ) fn load_default_font_bytes() -> Vec { if let Some( path ) = find_font_opt() { if let Ok( bytes ) = std::fs::read( &path ) { return bytes; } } crate::theme::fallback::FALLBACK_FONT.to_vec() }