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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// 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;

/// Frozen px sizes for the typographic scale, largest (`H0`, a display
/// heading) to smallest (`BODY_XS`, fine print). `H0`…`H3` are the heading
/// ramp; `BODY` is running text, with `BODY_S` / `BODY_XS` for secondary and
/// caption text. Each is the px size; for sizes that scale with the surface
/// use the responsive [`h0`]…[`body_xs`] functions instead.
pub const H0:       f32 = 50.0;
/// See [`H0`]. Px size of the second-largest heading level.
pub const H1:       f32 = 34.0;
/// See [`H0`]. Px size of the third heading level.
pub const H2:       f32 = 24.0;
/// See [`H0`]. Px size of the fourth heading level.
pub const H3:       f32 = 20.0;
/// See [`H0`]. Px size of running body text.
pub const BODY:     f32 = 16.0;
/// See [`H0`]. Px size of small (secondary) body text.
pub const BODY_S:   f32 = 14.0;
/// See [`H0`]. Px size of the smallest (caption) text.
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.

/// Responsive counterparts of the px constants: each returns a [`Length`]
/// that scales with the surface's smaller dimension (`vmin`) and is clamped
/// to a sensible px range, so the same level reads correctly from a portrait
/// phone to a 4K desktop. Same hierarchy as the constants — `h0` the largest
/// display heading down to `body_xs` the smallest caption. Largest display
/// heading: scales as 5% of `vmin`, clamped to `32..=80` px.
pub fn h0()      -> Length { Length::vmin( 5.0 ).clamp( 32.0, 80.0 ) }
/// See [`h0`]. Second heading level: 3.4% of `vmin`, clamped to `24..=56` px.
pub fn h1()      -> Length { Length::vmin( 3.4 ).clamp( 24.0, 56.0 ) }
/// See [`h0`]. Third heading level: 2.4% of `vmin`, clamped to `18..=40` px.
pub fn h2()      -> Length { Length::vmin( 2.4 ).clamp( 18.0, 40.0 ) }
/// See [`h0`]. Fourth heading level: 2.0% of `vmin`, clamped to `16..=32` px.
pub fn h3()      -> Length { Length::vmin( 2.0 ).clamp( 16.0, 32.0 ) }
/// See [`h0`]. Running body text: 1.6% of `vmin`, clamped to `14..=22` px.
pub fn body()    -> Length { Length::vmin( 1.6 ).clamp( 14.0, 22.0 ) }
/// See [`h0`]. Small body text: 1.4% of `vmin`, clamped to `12..=18` px.
pub fn body_s()  -> Length { Length::vmin( 1.4 ).clamp( 12.0, 18.0 ) }
/// See [`h0`]. Smallest caption text: 1.2% of `vmin`, clamped to `11..=15` px.
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 );
	}
}