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
// 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 tiny_skia::{ Path, PathBuilder };

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

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