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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Backend-neutral helpers for the software renderer: vector-path and
//! rounded-rect construction. System-font resolution lives in
//! `crate::system_fonts`.

use std::collections::hash_map::DefaultHasher;
use std::hash::{ Hash, Hasher };

use tiny_skia::{ Path, PathBuilder };

use crate::types::{ Corners, PathCmd };

/// What names an image's pixels in both backends' image caches: a fingerprint of them, or a key its caller vouches for, kept apart so that neither is taken for the other.
#[ derive( Clone, Copy, PartialEq, Eq, Hash ) ]
pub( crate ) enum ImageId
{
	Content( u64 ),
	Keyed( u64 ),
}

/// A 64-bit fingerprint of an RGBA buffer, the key of both backends' image
/// caches. Hashes the full slice below 16 KB (icons, thumbnails) and a
/// strided 8 × 512-byte sample above it, so a wallpaper does not pay a
/// full hash on every frame.
pub( crate ) fn fingerprint_rgba( bytes: &[u8] ) -> u64
{
	const FULL_HASH_THRESHOLD: usize = 16 * 1024;
	const SAMPLE_CHUNKS:       usize = 8;
	const SAMPLE_CHUNK_BYTES:  usize = 512;

	let mut h = DefaultHasher::new();
	let n     = bytes.len();
	n.hash( &mut h );
	if n <= FULL_HASH_THRESHOLD
	{
		bytes.hash( &mut h );
	} else {
		let stride = n / SAMPLE_CHUNKS;
		for i in 0..SAMPLE_CHUNKS
		{
			let pos = ( i * stride ).min( n - SAMPLE_CHUNK_BYTES );
			bytes[ pos..pos + SAMPLE_CHUNK_BYTES ].hash( &mut h );
		}
	}
	h.finish()
}

/// 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 tiny-skia path from a list of [`PathCmd`]s. Shared by both backends'
/// `fill_path` / `stroke_path`. Returns None for an empty or degenerate path.
pub ( crate ) fn build_ts_path( cmds: &[ PathCmd ] ) -> Option<Path>
{
	let mut pb = PathBuilder::new();
	for cmd in cmds
	{
		match *cmd
		{
			PathCmd::MoveTo( x, y )                  => pb.move_to( x, y ),
			PathCmd::LineTo( x, y )                  => pb.line_to( x, y ),
			PathCmd::QuadTo( x1, y1, x, y )          => pb.quad_to( x1, y1, x, y ),
			PathCmd::CubicTo( x1, y1, x2, y2, x, y ) => pb.cubic_to( x1, y1, x2, y2, x, y ),
			PathCmd::Close                           => pb.close(),
		}
	}
	pb.finish()
}

/// Validate the dimensions of an RGBA8 image buffer for `draw_image_data`,
/// shared by both backends. Returns `true` when the buffer is drawable;
/// `false` (after logging a one-line warning tagged with `backend`) when the
/// declared `img_w × img_h × 4` does not match `data.len()` or either extent is
/// zero, so the caller returns without uploading or seeding a cache key.
pub ( crate ) fn validate_rgba_dims( backend: &str, data: &[u8], img_w: u32, img_h: u32 ) -> bool
{
	let expected = ( img_w as usize ).saturating_mul( img_h as usize ).saturating_mul( 4 );
	if img_w == 0 || img_h == 0 || data.len() != expected
	{
		eprintln!(
			"[ltk] {}::draw_image_data: refusing draw — {}×{} declared, {} bytes provided, expected {}",
			backend, img_w, img_h, data.len(), expected,
		);
		return false;
	}
	true
}

/// 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()
}