// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! 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`, 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 ); } }