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 );
}
}