diff --git a/README.md b/README.md index 375b8ef3..5c6c56bc 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,18 @@ cargo add tui-test-rs@0.1.0-beta.1 ``` +Raster recording uses installed system fonts unless a JetBrains Mono bundle +feature is enabled: + +| Feature | Bundled JetBrains Mono faces | +| --- | --- | +| `recording-raster` | None; use installed system fonts | +| `recording-font-jetbrains-mono` | Full-glyph Regular | +| `recording-font-jetbrains-mono-styles` | Regular, Bold, Italic, and Bold Italic | +| `recording-font-jetbrains-mono-full` | All 16 static family faces | + +Each font feature enables `recording-raster`; the tiers are cumulative. + ```rust use tui_test::{OpenOptions, Operation, Session}; diff --git a/crates/tui-test/Cargo.toml b/crates/tui-test/Cargo.toml index 4f184e04..3a3e2848 100644 --- a/crates/tui-test/Cargo.toml +++ b/crates/tui-test/Cargo.toml @@ -16,6 +16,9 @@ path = "src/lib.rs" default = [] ghostty = ["dep:ghostty-vt"] recording-raster = ["dep:fontdb", "dep:tiny-skia"] +recording-font-jetbrains-mono = ["recording-raster"] +recording-font-jetbrains-mono-styles = ["recording-font-jetbrains-mono"] +recording-font-jetbrains-mono-full = ["recording-font-jetbrains-mono-styles"] [dependencies] alacritty_terminal.workspace = true diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Bold.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Bold.ttf new file mode 100644 index 00000000..cd1bee07 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Bold.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-BoldItalic.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-BoldItalic.ttf new file mode 100644 index 00000000..6ff8032a Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-BoldItalic.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraBold.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraBold.ttf new file mode 100644 index 00000000..437259bb Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraBold.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraBoldItalic.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraBoldItalic.ttf new file mode 100644 index 00000000..4b09b76c Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraBoldItalic.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraLight.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraLight.ttf new file mode 100644 index 00000000..0fc4d9b4 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraLight.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraLightItalic.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraLightItalic.ttf new file mode 100644 index 00000000..23ca9b08 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ExtraLightItalic.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Italic.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Italic.ttf new file mode 100644 index 00000000..71429b01 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Italic.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Light.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Light.ttf new file mode 100644 index 00000000..23df0998 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Light.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-LightItalic.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-LightItalic.ttf new file mode 100644 index 00000000..30e4b2c3 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-LightItalic.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Medium.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Medium.ttf new file mode 100644 index 00000000..dc2e5d08 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Medium.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-MediumItalic.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-MediumItalic.ttf new file mode 100644 index 00000000..d42c1354 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-MediumItalic.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Regular.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Regular.ttf new file mode 100644 index 00000000..711830ed Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Regular.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-SemiBold.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-SemiBold.ttf new file mode 100644 index 00000000..2bf8e48f Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-SemiBold.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-SemiBoldItalic.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-SemiBoldItalic.ttf new file mode 100644 index 00000000..133abcb3 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-SemiBoldItalic.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Thin.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Thin.ttf new file mode 100644 index 00000000..c78583e0 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-Thin.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ThinItalic.ttf b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ThinItalic.ttf new file mode 100644 index 00000000..58147ce5 Binary files /dev/null and b/crates/tui-test/assets/jetbrains-mono/JetBrainsMono-ThinItalic.ttf differ diff --git a/crates/tui-test/assets/jetbrains-mono/OFL.txt b/crates/tui-test/assets/jetbrains-mono/OFL.txt new file mode 100644 index 00000000..675d198b --- /dev/null +++ b/crates/tui-test/assets/jetbrains-mono/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/crates/tui-test/assets/jetbrains-mono/SOURCE b/crates/tui-test/assets/jetbrains-mono/SOURCE new file mode 100644 index 00000000..53ae3adf --- /dev/null +++ b/crates/tui-test/assets/jetbrains-mono/SOURCE @@ -0,0 +1,10 @@ +JetBrains Mono static TTF family +commit: 19371302b95d218af43299bce79ddbddd0bc364d +source: https://github.com/JetBrains/JetBrainsMono +path: fonts/ttf +files: official full-glyph static faces, not unicode subsets + +Feature tiers: +- recording-font-jetbrains-mono: Regular +- recording-font-jetbrains-mono-styles: Regular, Bold, Italic, Bold Italic +- recording-font-jetbrains-mono-full: all 16 non-NL static family faces diff --git a/crates/tui-test/src/render/font.rs b/crates/tui-test/src/render/font.rs index 2fee5c15..7893511a 100644 --- a/crates/tui-test/src/render/font.rs +++ b/crates/tui-test/src/render/font.rs @@ -4,6 +4,80 @@ use std::sync::{Arc, OnceLock}; pub(crate) const FAMILY: &str = "JetBrains Mono"; +#[cfg(feature = "recording-font-jetbrains-mono")] +const REGULAR_FONTS: &[&[u8]] = &[include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-Regular.ttf" +))]; + +#[cfg(feature = "recording-font-jetbrains-mono-styles")] +const STYLED_FONTS: &[&[u8]] = &[ + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-Bold.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-Italic.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-BoldItalic.ttf" + )), +]; + +#[cfg(feature = "recording-font-jetbrains-mono-full")] +const FULL_FAMILY_FONTS: &[&[u8]] = &[ + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-Thin.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-ThinItalic.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-ExtraLight.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-ExtraLightItalic.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-Light.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-LightItalic.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-Medium.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-MediumItalic.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-SemiBold.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-SemiBoldItalic.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-ExtraBold.ttf" + )), + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/JetBrainsMono-ExtraBoldItalic.ttf" + )), +]; + pub(crate) struct Catalog { pub database: Arc, candidates: [Vec; 4], @@ -36,6 +110,7 @@ pub(crate) fn catalog() -> &'static Catalog { static CATALOG: OnceLock = OnceLock::new(); CATALOG.get_or_init(|| { let mut database = fontdb::Database::new(); + load_bundled_fonts(&mut database); database.load_font_data(super::nerd_font::FONT_DATA.to_vec()); database.load_system_fonts(); @@ -68,6 +143,24 @@ pub(crate) fn catalog() -> &'static Catalog { }) } +fn load_bundled_fonts(database: &mut fontdb::Database) { + #[cfg(feature = "recording-font-jetbrains-mono")] + load_font_data(database, REGULAR_FONTS); + #[cfg(feature = "recording-font-jetbrains-mono-styles")] + load_font_data(database, STYLED_FONTS); + #[cfg(feature = "recording-font-jetbrains-mono-full")] + load_font_data(database, FULL_FAMILY_FONTS); + #[cfg(not(feature = "recording-font-jetbrains-mono"))] + let _ = database; +} + +#[cfg(feature = "recording-font-jetbrains-mono")] +fn load_font_data(database: &mut fontdb::Database, fonts: &[&[u8]]) { + for font in fonts { + database.load_font_data(font.to_vec()); + } +} + fn style_index(bold: bool, italic: bool) -> usize { usize::from(bold) | (usize::from(italic) << 1) } @@ -123,7 +216,7 @@ fn face_score( preferred: &[String], bold: bool, italic: bool, -) -> (usize, usize, u16, usize) { +) -> (usize, usize, usize, u16, usize) { let family = preferred .iter() .position(|preferred| { @@ -132,6 +225,7 @@ fn face_score( .any(|(family, _)| family.eq_ignore_ascii_case(preferred)) }) .unwrap_or(if face.monospaced { 1_000 } else { 2_000 }); + let exact_jetbrains_style = usize::from(!is_exact_jetbrains_style(face, bold, italic)); let wants_italic = italic; let is_italic = face.style != fontdb::Style::Normal; let style = usize::from(wants_italic != is_italic); @@ -142,12 +236,30 @@ fn face_score( }; ( family, + exact_jetbrains_style, style, face.weight.0.abs_diff(desired_weight), usize::from(!face.monospaced), ) } +fn is_exact_jetbrains_style(face: &fontdb::FaceInfo, bold: bool, italic: bool) -> bool { + if !face + .families + .iter() + .any(|(family, _)| family.eq_ignore_ascii_case(FAMILY)) + { + return false; + } + let expected = match (bold, italic) { + (false, false) => "JetBrainsMono-Regular", + (true, false) => "JetBrainsMono-Bold", + (false, true) => "JetBrainsMono-Italic", + (true, true) => "JetBrainsMono-BoldItalic", + }; + face.post_script_name.eq_ignore_ascii_case(expected) +} + #[cfg(test)] mod tests { use super::*; @@ -157,4 +269,94 @@ mod tests { let catalog = catalog(); assert!(!catalog.nerd_faces.is_empty()); } + + #[test] + fn selected_font_tier_has_the_expected_face_count() { + let expected = if cfg!(feature = "recording-font-jetbrains-mono-full") { + 16 + } else if cfg!(feature = "recording-font-jetbrains-mono-styles") { + 4 + } else if cfg!(feature = "recording-font-jetbrains-mono") { + 1 + } else { + 0 + }; + assert_eq!(bundled_font_count(), expected); + } + + #[cfg(feature = "recording-font-jetbrains-mono")] + #[test] + fn bundled_regular_font_is_the_full_official_face() { + let face = ttf_parser::Face::parse(REGULAR_FONTS[0], 0).unwrap(); + assert!(face.number_of_glyphs() >= 1_700); + } + + #[cfg(feature = "recording-font-jetbrains-mono-styles")] + #[test] + fn styled_tier_contains_bold_italic_and_bold_italic_faces() { + let metadata = font_metadata(STYLED_FONTS); + assert!(metadata.contains("JetBrainsMono-Bold")); + assert!(metadata.contains("JetBrainsMono-Italic")); + assert!(metadata.contains("JetBrainsMono-BoldItalic")); + } + + #[cfg(feature = "recording-font-jetbrains-mono-full")] + #[test] + fn full_tier_contains_every_remaining_static_family_face() { + let metadata = font_metadata(FULL_FAMILY_FONTS); + assert_eq!(metadata.len(), 12); + for face in [ + "JetBrainsMono-Thin", + "JetBrainsMono-ThinItalic", + "JetBrainsMono-ExtraLight", + "JetBrainsMono-ExtraLightItalic", + "JetBrainsMono-Light", + "JetBrainsMono-LightItalic", + "JetBrainsMono-Medium", + "JetBrainsMono-MediumItalic", + "JetBrainsMono-SemiBold", + "JetBrainsMono-SemiBoldItalic", + "JetBrainsMono-ExtraBold", + "JetBrainsMono-ExtraBoldItalic", + ] { + assert!(metadata.contains(face)); + } + } + + #[cfg(feature = "recording-font-jetbrains-mono")] + #[test] + fn bundled_font_includes_its_ofl_license_and_source() { + let license = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/OFL.txt" + )); + let source = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/jetbrains-mono/SOURCE" + )); + assert!(license.contains("SIL OPEN FONT LICENSE")); + assert!(source.contains("JetBrains/JetBrainsMono")); + assert!(source.contains("full-glyph")); + } + + fn bundled_font_count() -> usize { + let count = 0; + #[cfg(feature = "recording-font-jetbrains-mono")] + let count = count + REGULAR_FONTS.len(); + #[cfg(feature = "recording-font-jetbrains-mono-styles")] + let count = count + STYLED_FONTS.len(); + #[cfg(feature = "recording-font-jetbrains-mono-full")] + let count = count + FULL_FAMILY_FONTS.len(); + count + } + + #[cfg(feature = "recording-font-jetbrains-mono-styles")] + fn font_metadata(fonts: &[&[u8]]) -> HashSet { + let mut database = fontdb::Database::new(); + load_font_data(&mut database, fonts); + database + .faces() + .map(|face| face.post_script_name.clone()) + .collect() + } } diff --git a/crates/tui-test/src/render/raster/tests.rs b/crates/tui-test/src/render/raster/tests.rs index c9571c4f..3be8b3e6 100644 --- a/crates/tui-test/src/render/raster/tests.rs +++ b/crates/tui-test/src/render/raster/tests.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "recording-font-jetbrains-mono-styles")] +use super::font::{FontSystem, GlyphKey}; use super::{FrameRenderer, GridRenderer}; use crate::profile::Profile; use crate::record::frames::Frame; @@ -67,6 +69,23 @@ fn bold_and_italic_change_the_rasterized_glyph() { assert_ne!(regular.as_raw(), italic.as_raw()); } +#[cfg(feature = "recording-font-jetbrains-mono-styles")] +#[test] +fn bundled_styles_do_not_need_synthetic_bold_or_italic() { + let mut fonts = FontSystem::new(); + for (bold, italic) in [(false, false), (true, false), (false, true), (true, true)] { + let glyph = fonts + .resolve(GlyphKey { + character: 'M', + bold, + italic, + }) + .unwrap(); + assert!(!glyph.synthetic_bold); + assert!(!glyph.synthetic_italic); + } +} + #[test] fn supported_unicode_renders_and_missing_unicode_is_reported_when_absent() { let mut renderer = GridRenderer::new(1, 1);