ltk/render/
helpers.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! 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<Path>
{
	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<String>
{
	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<u8>
{
	if let Some( path ) = find_font_opt()
	{
		if let Ok( bytes ) = std::fs::read( &path )
		{
			return bytes;
		}
	}
	crate::theme::fallback::FALLBACK_FONT.to_vec()
}