ltk/theme/
typography.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
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Typography scale used by the default theme.
//!
//! Designed around the **Sora** typeface (Google Fonts). If Sora is not
//! installed on the system, ltk falls back to Liberation Sans / DejaVu Sans;
//! glyph metrics will differ slightly but the scale still reads correctly.
//!
//! Two scales coexist: the historic **px** constants (`H0` through
//! `BODY_XS`) for code that wants a frozen pixel size, and the new
//! **responsive** scale ([`h0`], [`h1`], …, [`body_xs`]) that returns
//! viewport-relative [`crate::Length`] values clamped to the same px
//! range that used to be the constant. Call sites can mix freely:
//! `.size( typography::H2 )` still resolves to `Length::px( 24.0 )` via
//! `From<f32>`, while `.size( typography::h2() )` scales with the
//! surface's smaller dimension.

use crate::types::Length;

pub const H0:       f32 = 50.0;
pub const H1:       f32 = 34.0;
pub const H2:       f32 = 24.0;
pub const H3:       f32 = 20.0;
pub const BODY:     f32 = 16.0;
pub const BODY_S:   f32 = 14.0;
pub const BODY_XS:  f32 = 12.0;

/// Interlineado (line-height) multiplier recommended by the kit. Apply as
/// `size * LINE_HEIGHT` when laying out multi-line text blocks.
pub const LINE_HEIGHT: f32 = 1.5;

// ─── Responsive scale ────────────────────────────────────────────────────────
//
// The percentages are calibrated so the px clamps match the legacy constants
// at a 1000 px logical smaller side (a typical landscape tablet). On a Librem 5
// portrait (~720 px) headings round down sensibly; on a 4K desktop the upper
// clamp kicks in before display titles get absurd.

pub fn h0()      -> Length { Length::vmin( 5.0 ).clamp( 32.0, 80.0 ) }
pub fn h1()      -> Length { Length::vmin( 3.4 ).clamp( 24.0, 56.0 ) }
pub fn h2()      -> Length { Length::vmin( 2.4 ).clamp( 18.0, 40.0 ) }
pub fn h3()      -> Length { Length::vmin( 2.0 ).clamp( 16.0, 32.0 ) }
pub fn body()    -> Length { Length::vmin( 1.6 ).clamp( 14.0, 22.0 ) }
pub fn body_s()  -> Length { Length::vmin( 1.4 ).clamp( 12.0, 18.0 ) }
pub fn body_xs() -> Length { Length::vmin( 1.2 ).clamp( 11.0, 15.0 ) }

#[ cfg( test ) ]
mod tests
{
	use super::*;

	/// 360-px-wide portrait phone — Vmin ratio under the clamp's lower bound,
	/// so every scale snaps to its `min_px`.
	#[ test ]
	fn responsive_scale_pins_to_min_on_narrow_phones()
	{
		let vp = ( 360.0, 720.0 );
		let em = Length::EM_BASE_DEFAULT;
		assert_eq!( h0().resolve( vp, em ),      32.0 );
		assert_eq!( body().resolve( vp, em ),    14.0 );
		assert_eq!( body_xs().resolve( vp, em ), 11.0 );
	}

	/// 1000-px smaller side — the calibration point. Numbers should be
	/// "around" the legacy px constants without exceeding the upper clamp.
	#[ test ]
	fn responsive_scale_centers_around_legacy_px_constants()
	{
		let vp = ( 1000.0, 1000.0 );
		let em = Length::EM_BASE_DEFAULT;
		assert_eq!( h0().resolve( vp, em ),   50.0 );
		assert_eq!( h2().resolve( vp, em ),   24.0 );
		assert_eq!( body().resolve( vp, em ), 16.0 );
	}

	/// 4K-class smaller side — every scale should saturate to its upper
	/// clamp instead of growing absurdly.
	#[ test ]
	fn responsive_scale_pins_to_max_on_large_displays()
	{
		let vp = ( 2160.0, 3840.0 );
		let em = Length::EM_BASE_DEFAULT;
		assert_eq!( h0().resolve( vp, em ),   80.0 );
		assert_eq!( body().resolve( vp, em ), 22.0 );
	}
}