From 2d325dd78583cd1d8797fb40a27f86a1fa2937cb Mon Sep 17 00:00:00 2001 From: Anton Suprunchuk Date: Mon, 5 Jan 2026 12:36:06 -0300 Subject: [PATCH 1/6] fix clippy --- Cargo.lock | 111 ++++++++++++++++++++++++++-- Cargo.toml | 2 +- examples/text.rs | 16 ++-- src/buffer_utils.rs | 42 +++++++++-- src/font_family_query.rs | 97 ++++++++++++++++++++++++ src/lib.rs | 1 + src/state.rs | 21 +++++- src/style.rs | 155 ++++++++++++++++++++++++++++++++++----- src/tests/mod.rs | 4 +- src/tests/text_state.rs | 6 +- src/text_manager.rs | 4 + src/text_params.rs | 23 +++++- src/utils.rs | 17 ++++- 13 files changed, 453 insertions(+), 46 deletions(-) create mode 100644 src/font_family_query.rs diff --git a/Cargo.lock b/Cargo.lock index 9167b24..3da604d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,6 +388,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "cosmic-text" version = "0.14.2" @@ -395,7 +404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" dependencies = [ "bitflags 2.9.1", - "fontdb", + "fontdb 0.16.2", "log", "rangemap", "rustc-hash 1.1.0", @@ -411,6 +420,30 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cosmic-text" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cadaea21e24c49c0c82116f2b465ae6a49d63c90e428b0f8d9ae1f638ac91f" +dependencies = [ + "bitflags 2.9.1", + "fontdb 0.23.0", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 1.1.0", + "self_cell", + "skrifa 0.39.0", + "smol_str 0.2.2", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -550,6 +583,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.8" @@ -573,6 +615,20 @@ dependencies = [ "ttf-parser 0.20.0", ] +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.25.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -749,7 +805,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6a289ad2a23656ccf4306fc818cef6776471a136d909123fd26c0f2ecb44ba" dependencies = [ - "cosmic-text", + "cosmic-text 0.14.2", "etagere", "lru 0.12.5", "rustc-hash 2.1.1", @@ -834,6 +890,19 @@ dependencies = [ "num-traits", ] +[[package]] +name = "harfrust" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0caaee032384c10dd597af4579c67dee16650d862a9ccbe1233ff1a379abc07" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "core_maths", + "read-fonts 0.36.0", + "smallvec", +] + [[package]] name = "hashbrown" version = "0.15.3" @@ -1001,6 +1070,12 @@ dependencies = [ "redox_syscall 0.5.12", ] +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1625,7 +1700,7 @@ name = "protextinator" version = "0.4.0" dependencies = [ "ahash", - "cosmic-text", + "cosmic-text 0.16.0", "env_logger", "futures", "grafo", @@ -1684,7 +1759,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d" dependencies = [ "bytemuck", - "font-types", + "font-types 0.9.0", +] + +[[package]] +name = "read-fonts" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.10.1", ] [[package]] @@ -1898,7 +1984,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.29.3", +] + +[[package]] +name = "skrifa" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9eb0b904a04d09bd68c65d946617b8ff733009999050f3b851c32fb3cfb60e" +dependencies = [ + "bytemuck", + "read-fonts 0.36.0", ] [[package]] @@ -2024,7 +2120,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f745de914febc7c9ab4388dfaf94bbc87e69f57bb41133a9b0c84d4be49856f3" dependencies = [ - "skrifa", + "skrifa 0.31.3", "yazi", "zeno", ] @@ -2188,6 +2284,9 @@ name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] [[package]] name = "unicode-bidi" diff --git a/Cargo.toml b/Cargo.toml index 1893d2e..6a7c9de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ default = [] serialization = ["dep:serde"] [dependencies] -cosmic-text = "0.14" +cosmic-text = "0.16.0" ahash = "0.8.12" smol_str = "0.3" serde = { version = "1.0.219", features = ["derive"], optional = true } diff --git a/examples/text.rs b/examples/text.rs index e82142e..f3ecb46 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -133,7 +133,9 @@ impl<'a> App<'a> { horizontal_alignment: HorizontalTextAlignment::Start, vertical_alignment: VerticalTextAlignment::Start, wrap: Some(TextWrap::Wrap), - font_family: FontFamily::SansSerif, + font_family: FontFamily::Name("Arial".into()), + weight: protextinator::style::Weight::NORMAL, + letter_spacing: None, }; // Create or update the text state @@ -191,6 +193,8 @@ impl<'a> App<'a> { vertical_alignment: VerticalTextAlignment::Start, wrap: Some(TextWrap::Wrap), font_family: FontFamily::Serif, + weight: protextinator::style::Weight::NORMAL, + letter_spacing: None, }; // Create or update the stats text state @@ -262,11 +266,11 @@ impl<'a> App<'a> { } let upload_time = t_upload_start.elapsed(); - println!( - "rasterize: {} µs, load_texture: {} µs", - raster_time.as_micros(), - upload_time.as_micros() - ); + // println!( + // "rasterize: {} µs, load_texture: {} µs", + // raster_time.as_micros(), + // upload_time.as_micros() + // ); // TODO: cache shapes let text_shape_id = renderer.add_shape( diff --git a/src/buffer_utils.rs b/src/buffer_utils.rs index b431526..8da2c3c 100644 --- a/src/buffer_utils.rs +++ b/src/buffer_utils.rs @@ -1,6 +1,6 @@ use crate::byte_cursor::ByteCursor; use crate::math::{Point, Rect, Size}; -use crate::style::{TextStyle, TextWrap, VerticalTextAlignment}; +use crate::style::{FontFamily, TextStyle, TextWrap, VerticalTextAlignment}; use crate::text_params::TextParams; use cosmic_text::{Attrs, Buffer, Cursor, Edit, Editor, FontSystem, Shaping}; @@ -120,13 +120,15 @@ pub(crate) fn update_buffer( params: &TextParams, buffer: &mut Buffer, font_system: &mut FontSystem, + font_family: &FontFamily, ) -> Size { let text_style = ¶ms.style(); let font_color = text_style.font_color; let horizontal_alignment = text_style.horizontal_alignment; let wrap = text_style.wrap.unwrap_or_default(); let text_area_size = params.size(); - let font_family = &text_style.font_family; + let weight = text_style.weight; + let letter_spacing = text_style.letter_spacing; let metadata = params.metadata(); let old_scroll = buffer.scroll(); @@ -140,14 +142,37 @@ pub(crate) fn update_buffer( // Apply scale for shaping to device pixels buffer.set_size(font_system, Some(text_area_size.x * scale_factor), None); + // match font_family { + // FontFamily::Name(name) => { + // println!("Using font family: {:?}", name); + // } + // FontFamily::SansSerif => {} + // FontFamily::Serif => {} + // FontFamily::Monospace => {} + // FontFamily::Cursive => {} + // FontFamily::Fantasy => {} + // } + + if params.original_text() == "Filobus" { + println!("Font family: {:?}", font_family); + } + + let mut attrs = Attrs::new() + .color(font_color.into()) + .family(font_family.to_fontdb_family()) + .weight(weight.into()) + .metadata(metadata); + + if let Some(letter_spacing) = letter_spacing { + attrs = attrs.letter_spacing(letter_spacing.0 * scale_factor); + } + buffer.set_text( font_system, params.text_for_internal_use(), - &Attrs::new() - .color(font_color.into()) - .family(font_family.to_fontdb_family()) - .metadata(metadata), + &attrs, Shaping::Advanced, + None, ); let mut buffer_measurement = Size::default(); @@ -162,12 +187,14 @@ pub(crate) fn update_buffer( None, // TODO: what is the default tab width? Make it configurable? 2, + cosmic_text::Hinting::Enabled, ) .iter() { - buffer_measurement.y += layout_line + let line_height = layout_line .line_height_opt .unwrap_or(text_style.line_height_pt() * scale_factor); + buffer_measurement.y += line_height; buffer_measurement.x = buffer_measurement.x.max(layout_line.w); } } @@ -187,6 +214,7 @@ pub(crate) fn update_buffer( None, // TODO: what is the default tab width? Make it configurable? 2, + cosmic_text::Hinting::Enabled, ); } } diff --git a/src/font_family_query.rs b/src/font_family_query.rs new file mode 100644 index 0000000..a8d611c --- /dev/null +++ b/src/font_family_query.rs @@ -0,0 +1,97 @@ +use crate::style::{FontFamily, Weight}; +use crate::utils::ArcCowStr; +use ahash::HashMap; +use cosmic_text::{fontdb, FontSystem}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct FontFamilyQuery { + pub family_query_string: ArcCowStr, + pub weight: Weight, +} + +impl FontFamilyQuery { + pub fn split_families(&self) -> impl Iterator + use<'_> { + self.family_query_string + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| { + let unquoted = s + .strip_prefix(['\'', '"']) + .and_then(|s| s.strip_suffix(['\'', '"'])) + .unwrap_or(s); + FontFamily::parse(unquoted) + }) + } +} + +pub struct FontFamilyCache { + /// This cache maps font family query strings to the first resolved font family name + /// that exists on the current platform. + /// For example, a query `"Helvetica, 'Segoe UI'"` will map to `"Helvetica"` on a Mac and + /// `"Segoe UI"` on Windows. + font_family_query_to_resolved_family_cache: HashMap, +} + +impl FontFamilyCache { + pub fn new() -> Self { + Self { + font_family_query_to_resolved_family_cache: HashMap::default(), + } + } + + pub(crate) fn resolve_font_family_query( + &mut self, + query: FontFamilyQuery, + font_system: &mut FontSystem, + ) -> FontFamily { + if let Some(cached) = self.font_family_query_to_resolved_family_cache.get(&query) { + return cached.clone(); + } + + for family in query.split_families() { + let res = font_system.db().query(&fontdb::Query { + families: &[family.to_fontdb_family()], + weight: fontdb::Weight::NORMAL, + ..Default::default() + }); + if res.is_some() { + self.font_family_query_to_resolved_family_cache + .insert(query.clone(), family.clone()); + return family; + } + } + + // Fallback to SansSerif if no family is found + let fallback = FontFamily::SansSerif; + self.font_family_query_to_resolved_family_cache + .insert(query, fallback.clone()); + fallback + } +} + +#[cfg(test)] +mod test { + use crate::font_family_query::FontFamilyQuery; + use crate::style::{FontFamily, Weight}; + use crate::utils::ArcCowStr; + + #[test] + pub fn should_split_families_correctly() { + let query = FontFamilyQuery { + family_query_string: ArcCowStr::from("Helvetica, 'Segoe UI', Arial, sans-serif"), + weight: Weight::NORMAL, + }; + + let families: Vec = query.split_families().collect(); + assert_eq!( + families, + vec![ + FontFamily::Name("Helvetica".into()), + FontFamily::Name("Segoe UI".into()), + FontFamily::Name("Arial".into()), + FontFamily::SansSerif, + ] + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1e033ea..f2de81e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,6 +65,7 @@ mod action; mod buffer_utils; mod byte_cursor; +mod font_family_query; mod id; pub mod math; mod state; diff --git a/src/state.rs b/src/state.rs index 20d4885..1517cfe 100644 --- a/src/state.rs +++ b/src/state.rs @@ -10,7 +10,7 @@ use crate::buffer_utils::{ }; use crate::byte_cursor::ByteCursor; use crate::math::Size; -use crate::style::{TextStyle, VerticalTextAlignment}; +use crate::style::{FontFamily, TextStyle, VerticalTextAlignment}; use crate::text_manager::TextContext; use crate::text_params::TextParams; use crate::utils::{linear_to_srgb_u8, srgb_to_linear_u8}; @@ -113,6 +113,7 @@ pub struct TextState { relative_caret_position: Option, caret_width: f32, selection: Selection, + resolved_font_family: FontFamily, last_scroll_timestamp: Instant, @@ -183,6 +184,8 @@ impl TextState { cursor: ByteCursor::default(), relative_caret_position: None, + resolved_font_family: FontFamily::SansSerif, + selection: Selection::default(), last_scroll_timestamp: Instant::now(), scroll_interval: Duration::from_millis(50), @@ -1055,9 +1058,23 @@ impl TextState { /// This method checks if any text parameters (content, style, size) have changed /// and only performs the expensive reshape operation if necessary. fn reshape_if_params_changed(&mut self, ctx: &mut TextContext) { + let font_query_changed = self.params.font_query_changed_since_last_shape(); + if font_query_changed { + let new_font_family = ctx.font_family_cache.resolve_font_family_query( + self.params.style().font_family_query(), + &mut ctx.font_system, + ); + self.resolved_font_family = new_font_family; + self.params.reset_font_query_changed(); + } let params_changed = self.params.changed_since_last_shape(); if params_changed { - let new_size = update_buffer(&self.params, &mut self.buffer, &mut ctx.font_system); + let new_size = update_buffer( + &self.params, + &mut self.buffer, + &mut ctx.font_system, + &self.resolved_font_family, + ); self.inner_dimensions = new_size; self.params.reset_changed(); // Any layout/text/style/size change requires re-rasterization diff --git a/src/style.rs b/src/style.rs index ab0ab36..8776b96 100644 --- a/src/style.rs +++ b/src/style.rs @@ -3,6 +3,7 @@ //! This module provides comprehensive text styling capabilities including fonts, //! colors, alignment, wrapping, and other visual properties for text rendering. +use crate::font_family_query::FontFamilyQuery; use crate::utils::ArcCowStr; use cosmic_text::{Align, Color, Family}; #[cfg(feature = "serialization")] @@ -140,6 +141,46 @@ impl From for FontSize { impl Eq for FontSize {} +/// Represents the spacing between characters +#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct LetterSpacing(pub f32); + +impl Hash for LetterSpacing { + fn hash(&self, state: &mut H) { + self.0.to_bits().hash(state); + } +} + +impl From for LetterSpacing { + /// Creates a `LetterSpacing` from a floating-point multiplier. + /// + /// # Examples + /// ``` + /// use protextinator::style::LetterSpacing; + /// + /// let line_height: LetterSpacing = 1.2.into(); + /// assert_eq!(line_height.0, 1.2); + /// ``` + fn from(value: f32) -> Self { + Self(value) + } +} + +impl Eq for LetterSpacing {} + +impl From for cosmic_text::LetterSpacing { + fn from(letter_spacing: LetterSpacing) -> Self { + cosmic_text::LetterSpacing(letter_spacing.0) + } +} + +impl LetterSpacing { + pub const fn new(spacing: f32) -> Self { + Self(spacing) + } +} + /// Wrapper around [`cosmic_text::Color`] for text color representation. /// /// Provides convenient constructors for creating colors from RGB and RGBA values. @@ -336,29 +377,58 @@ impl FontFamily { FontFamily::Fantasy => Family::Fantasy, } } + + /// Parses a string into a `FontFamily`. + /// + /// Recognizes standard family names like "serif", "sans-serif", "monospace", "cursive", and "fantasy". + /// Any other string is treated as a named font family. + /// + /// # Arguments + /// * `string` - The font family name as a string slice. + /// + /// # Examples + /// ``` + /// use protextinator::style::FontFamily; + /// + /// let serif = FontFamily::parse("serif"); + /// let custom = FontFamily::parse("MyCustomFont"); + /// ``` + pub fn parse(string: &str) -> Self { + match string { + "serif" => Self::Serif, + "monospace" => Self::Monospace, + "cursive" => Self::Cursive, + "fantasy" => Self::Fantasy, + "sans-serif" => Self::SansSerif, + name => Self::Name(name.to_string().into()), + } + } } -/// Comprehensive text styling configuration. -/// -/// `TextStyle` combines all visual aspects of text rendering, including font properties, -/// colors, alignment, and wrapping behavior #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TextStyle { - /// The font size in points. - pub font_size: FontSize, - /// The line height is a multiplier of the font size. - pub line_height: LineHeight, - /// The color of the text. - pub font_color: FontColor, - /// Horizontal text alignment within the container. - pub horizontal_alignment: HorizontalTextAlignment, - /// Vertical text alignment within the container. - pub vertical_alignment: VerticalTextAlignment, - /// Text wrapping behavior. - pub wrap: Option, - /// The font family to use for rendering. - pub font_family: FontFamily, +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct Weight(pub u16); + +impl Weight { + pub const THIN: Weight = Weight(100); + pub const EXTRA_LIGHT: Weight = Weight(200); + pub const LIGHT: Weight = Weight(300); + pub const NORMAL: Weight = Weight(400); + pub const MEDIUM: Weight = Weight(500); + pub const SEMIBOLD: Weight = Weight(600); + pub const BOLD: Weight = Weight(700); + pub const EXTRA_BOLD: Weight = Weight(800); + pub const BLACK: Weight = Weight(900); + + pub fn new(weight: u16) -> Self { + Self(weight) + } +} + +impl From for cosmic_text::Weight { + fn from(weight: Weight) -> Self { + cosmic_text::Weight(weight.0) + } } /// Horizontal text alignment options. @@ -422,6 +492,33 @@ pub enum VerticalTextAlignment { Center, } +/// Comprehensive text styling configuration. +/// +/// `TextStyle` combines all visual aspects of text rendering, including font properties, +/// colors, alignment, and wrapping behavior +#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TextStyle { + /// The font size in points. + pub font_size: FontSize, + /// The line height is a multiplier of the font size. + pub line_height: LineHeight, + /// The color of the text. + pub font_color: FontColor, + /// Horizontal text alignment within the container. + pub horizontal_alignment: HorizontalTextAlignment, + /// Vertical text alignment within the container. + pub vertical_alignment: VerticalTextAlignment, + /// Text wrapping behavior. + pub wrap: Option, + /// The font family to use for rendering. + pub font_family: FontFamily, + /// The font weight to use for rendering. + pub weight: Weight, + /// The spacing between characters as a multiplier of the font size. + pub letter_spacing: Option, +} + impl Hash for TextStyle { fn hash(&self, state: &mut H) { self.font_size.hash(state); @@ -459,6 +556,8 @@ impl TextStyle { vertical_alignment: VerticalTextAlignment::Start, wrap: None, font_family: FontFamily::SansSerif, + weight: Weight::NORMAL, + letter_spacing: None, }; /// Creates a new `TextStyle` with the specified font size and color. @@ -485,6 +584,8 @@ impl TextStyle { vertical_alignment: VerticalTextAlignment::Start, wrap: None, font_family: FontFamily::SansSerif, + weight: Weight::NORMAL, + letter_spacing: None, } } @@ -625,4 +726,18 @@ impl TextStyle { pub const fn line_height_pt(&self) -> f32 { self.line_height.0 * self.font_size.0 } + + pub(crate) fn font_family_query(&self) -> FontFamilyQuery { + FontFamilyQuery { + family_query_string: match &self.font_family { + FontFamily::Name(name) => name.clone(), + FontFamily::SansSerif => "sans-serif".into(), + FontFamily::Serif => "serif".into(), + FontFamily::Monospace => "monospace".into(), + FontFamily::Cursive => "cursive".into(), + FontFamily::Fantasy => "fantasy".into(), + }, + weight: self.weight, + } + } } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 8bc9a50..3f6993e 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,6 +1,6 @@ use crate::style::{ FontColor, FontFamily, FontSize, HorizontalTextAlignment, LineHeight, TextStyle, TextWrap, - VerticalTextAlignment, + VerticalTextAlignment, Weight, }; use cosmic_text::Color; @@ -19,5 +19,7 @@ fn mono_style_test() -> TextStyle { vertical_alignment: VerticalTextAlignment::Start, wrap: Some(TextWrap::NoWrap), // No wrapping to ensure a single line font_family: FontFamily::Monospace, + weight: Weight::NORMAL, + letter_spacing: None, } } diff --git a/src/tests/text_state.rs b/src/tests/text_state.rs index c39f1c3..09975de 100644 --- a/src/tests/text_state.rs +++ b/src/tests/text_state.rs @@ -1,7 +1,7 @@ use crate::math::Size; use crate::style::{ FontColor, FontFamily, FontSize, HorizontalTextAlignment, LineHeight, TextStyle, TextWrap, - VerticalTextAlignment, + VerticalTextAlignment, Weight, }; use crate::tests::mono_style_test; use crate::{Action, Point, TextContext, TextState}; @@ -20,6 +20,8 @@ fn mono_style_with_alignment( vertical_alignment: v_align, wrap: Some(TextWrap::NoWrap), // No wrapping to ensure a single line font_family: FontFamily::Monospace, + weight: Weight::NORMAL, + letter_spacing: None, } } @@ -36,6 +38,8 @@ fn mono_style_with_wrap( vertical_alignment: v_align, wrap, font_family: FontFamily::Monospace, + weight: Weight::NORMAL, + letter_spacing: None, } } diff --git a/src/text_manager.rs b/src/text_manager.rs index 07376ca..33d0132 100644 --- a/src/text_manager.rs +++ b/src/text_manager.rs @@ -3,6 +3,7 @@ //! This module provides high-level management of multiple text states, font loading, //! and resource tracking for text rendering systems. +use crate::font_family_query::FontFamilyCache; use crate::state::{AlphaMode, TextState}; use crate::Id; use ahash::{HashMap, HashSet, HashSetExt}; @@ -22,6 +23,8 @@ pub struct TextContext { pub scale_factor: f32, /// Tracks which text states are being used for garbage collection. pub usage_tracker: TextUsageTracker, + /// Cache for resolved font family queries. + pub font_family_cache: FontFamilyCache, } impl Default for TextContext { @@ -32,6 +35,7 @@ impl Default for TextContext { swash_cache: SwashCache::new(), scale_factor: 1.0, usage_tracker: TextUsageTracker::new(), + font_family_cache: FontFamilyCache::new(), } } } diff --git a/src/text_params.rs b/src/text_params.rs index cd653e0..b73ebe8 100644 --- a/src/text_params.rs +++ b/src/text_params.rs @@ -14,6 +14,7 @@ pub(crate) struct TextParams { scale_factor: f32, changed: bool, + font_query_changed: bool, line_terminator_has_been_added: bool, } @@ -28,6 +29,7 @@ impl TextParams { scale_factor: 1.0, changed: true, + font_query_changed: true, line_terminator_has_been_added: false, }; @@ -112,11 +114,25 @@ impl TextParams { self.changed = false; } + #[inline(always)] + pub fn font_query_changed_since_last_shape(&self) -> bool { + self.font_query_changed + } + + #[inline(always)] + pub fn reset_font_query_changed(&mut self) { + self.font_query_changed = false; + } + #[inline(always)] pub fn set_text(&mut self, text: &str) { if self.original_text() != text { self.text = text.into(); - if !self.text.ends_with('\n') { + // TODO: move that to the insertion of the newline - this will cause shenanigans + // if text is one line right now, but you intend to add a newline later + let is_one_line = !self.text.contains('\n'); + let ends_with_line_terminator = !self.text.ends_with('\n'); + if !is_one_line && !ends_with_line_terminator { // Ensure the text always ends with a line terminator. If the text does not end with // a newline, you'll need to add two newline characters to insert a new line at the // end of the text. @@ -139,6 +155,11 @@ impl TextParams { #[inline(always)] pub fn set_style(&mut self, style: &TextStyle) { + let font_query_changed = + self.style.font_family != style.font_family || self.style.weight != style.weight; + if font_query_changed { + self.font_query_changed = true; + } if &self.style != style { self.style = style.clone(); self.changed = true; diff --git a/src/utils.rs b/src/utils.rs index f8debc7..ebe4ac7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,6 +5,7 @@ #[cfg(feature = "serialization")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::hash::Hash; use std::{ops::Deref, sync::Arc}; /// An efficient string type that can hold either borrowed static strings or owned arc strings. @@ -12,7 +13,7 @@ use std::{ops::Deref, sync::Arc}; /// This type is optimized for cases where strings are frequently either static string literals /// or shared owned strings. It avoids unnecessary allocations when dealing with static strings /// while providing reference counting for owned strings. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug)] pub enum ArcCowStr { /// A borrowed static string slice with 'static lifetime. Borrowed(&'static str), @@ -69,6 +70,20 @@ impl Deref for ArcCowStr { } } +impl Hash for ArcCowStr { + fn hash(&self, state: &mut H) { + self.deref().hash(state); + } +} + +impl PartialEq for ArcCowStr { + fn eq(&self, other: &Self) -> bool { + self.deref() == other.deref() + } +} + +impl Eq for ArcCowStr {} + #[cfg(feature = "serialization")] impl Serialize for ArcCowStr { fn serialize(&self, serializer: S) -> Result From 5d1669bd8f7cabf45495533489cce53d4470bcbb Mon Sep 17 00:00:00 2001 From: Anton Suprunchuk Date: Sun, 18 Jan 2026 17:38:42 -0300 Subject: [PATCH 2/6] bump version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3da604d..5146dba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1697,7 +1697,7 @@ checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" [[package]] name = "protextinator" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ahash", "cosmic-text 0.16.0", diff --git a/Cargo.toml b/Cargo.toml index 6a7c9de..e37e5dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "protextinator" -version = "0.4.0" +version = "0.5.0" edition = "2021" description = "Text management, made simple" keywords = ["text", "rendering", "gui", "graphics", "image"] From a56eb33ced089e06201756b231c45d56a91b6d91 Mon Sep 17 00:00:00 2001 From: Anton Suprunchuk Date: Sun, 18 Jan 2026 17:50:25 -0300 Subject: [PATCH 3/6] update README.md --- README.md | 128 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 73 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 741aca6..e647ce4 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,99 @@ -# 🚀 Protextinator +# Protextinator -
- Crates.io - Docs.rs - License -
+[![Crates.io](https://img.shields.io/crates/v/protextinator.svg)](https://crates.io/crates/protextinator) +[![Docs.rs](https://img.shields.io/docsrs/protextinator)](https://docs.rs/protextinator/latest/protextinator/) +[![License](https://img.shields.io/crates/l/protextinator)](https://github.com/antouhou/protextinator/blob/main/LICENSE_MIT) -## ✨ Text Management, Made Simple! +Protextinator is a text editing and rendering library for Rust, built on top of [cosmic_text](https://github.com/pop-os/cosmic-text). It provides a simpler API while adding features like vertical alignment, scroll position management, and text selection. -**Protextinator** is a powerful text editing and rendering library built on top of [cosmic_text](https://github.com/pop-os/cosmic-text), providing a simpler API with advanced features for all your text handling needs! +**Note:** This library is still a work in progress. APIs may change. -> 💡 Perfect for game UIs, text editors, and any application that needs sophisticated text rendering with minimal hassle. +## Features -⚠️ **WARNING**: This library is work in progress, use at your own risk! ⚠️ +- Vertical text alignment +- Text buffer size measurement +- Scroll position management with absolute coordinates +- Simple font loading from files or embedded bytes +- Text state collection with optional usage tracking +- Custom metadata for text states +- Text selection and editing +- Efficient text buffer caching +- Word wrapping and text styling +- Optional serialization support -## 🔥 Features +## Installation -- **Vertical text alignment** - Position your text exactly where you want it -- **Text buffer size measurement** - Know exactly how much space your text needs -- **Scroll position management** with absolute coordinates -- **Simple font loading interface** - Load fonts from files or embedded bytes -- **Text state collection** with optional usage tracking for garbage collection -- **Custom metadata** for text states -- **Text selection and editing** capabilities -- **Efficient text buffer caching** -- **Word wrapping and text styling** -- **Optional serialization** support via the `serialization` feature - -## 📦 Installation - -Add Protextinator to your `Cargo.toml`: +Add this to your `Cargo.toml`: ```toml [dependencies] -protextinator = "0.1.0" +protextinator = "0.5.0" ``` -With serialization support: +With serialization: ```toml [dependencies] protextinator = { version = "0.1.0", features = ["serialization"] } ``` -## 🚀 Quick Start - -For code examples and detailed usage, check out: -- [Documentation on docs.rs](https://docs.rs/protextinator/) -- [Example code in the repository](https://github.com/antouhou/protextinator/tree/main/examples) - -Protextinator makes it easy to: -1. Create and manage text states -2. Style text with various fonts, colors, and alignments -3. Handle text selection and editing -4. Efficiently render text in your application - -## 🎮 Integration Example - -Protextinator works great with rendering libraries like [Grafo](https://github.com/antouhou/grafo) and windowing libraries like [Winit](https://github.com/rust-windowing/winit). Check out the examples directory for a complete integration example. - -## 📚 API Overview - -- **TextManager**: The main entry point for managing text states and fonts -- **TextState**: Represents a text buffer with styling and layout information -- **Id**: A unique identifier for text states -- **TextStyle**: Configure font size, color, alignment, and more -- **Action**: Perform operations like copy, paste, and cursor movement - -## 🔧 Contributing +## Quick Start + +```rust +use protextinator::{TextManager, TextState, math::Size}; +use cosmic_text::{fontdb, Color}; +use protextinator::style::TextStyle; + +// Create a text manager +let mut text_manager = TextManager::new(); + +// Create a text state +let id = protextinator::Id::new("my_text"); +let text = "Hello, world!"; +text_manager.create_state(id, text, ()); +// Add fonts +let font_sources: Vec = vec![]; +text_manager.load_fonts(font_sources.into_iter()); +// Alternatively, you can load fonts from bytes if you want to embed them into the binary +// or download them at runtime as bytes +let byte_sources: Vec<&'static [u8]> = vec![]; +text_manager.load_fonts_from_bytes(byte_sources.into_iter()); + +// Optional: Marks the beginning of a frame so that you can track which text states are accessed +text_manager.start_frame(); + +// Configure the text area size and style +if let Some(state) = text_manager.text_states.get_mut(&id) { + state.set_outer_size(&Size::new(400.0, 200.0)); + + let style = TextStyle::new(16.0, Color::rgb(255, 255, 255)) + .with_line_height(1.5); + state.set_style(&style); + + // Enable editing + state.is_editable = true; + state.is_selectable = true; + state.are_actions_enabled = true; + + // Recalculate layout + state.recalculate(&mut text_manager.text_context); + + // Get the inner size of the buffer - i.e., how much space the text needs to occupy + let inner_size = state.inner_size(); +} + +let mut remove_ids = vec![]; +// Optional: going to remove all states that were not accessed during the current frame +text_manager.end_frame(&mut remove_ids); +``` -Contributions are welcome! Feel free to open issues or submit pull requests. +For a complete example, see the [examples directory](https://github.com/antouhou/protextinator/tree/main/examples). -## 📄 License +## License Licensed under either of: -- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +- Apache License, Version 2.0 ([LICENSE-APACHE](http://www.apache.org/licenses/LICENSE-2.0)) - MIT license ([LICENSE-MIT](LICENSE_MIT) or http://opensource.org/licenses/MIT) at your option. From a089bb75d8259189fbd8b09fdbde58a854030315 Mon Sep 17 00:00:00 2001 From: Anton Suprunchuk Date: Sun, 18 Jan 2026 19:40:44 -0300 Subject: [PATCH 4/6] fix font family fallback --- examples/text.rs | 10 +++--- src/buffer_utils.rs | 17 ++------- src/font_family_query.rs | 31 ++++++++++++----- src/state.rs | 19 ++++++++++ src/tests/mod.rs | 1 + src/tests/resolved_font_family.rs | 58 +++++++++++++++++++++++++++++++ 6 files changed, 107 insertions(+), 29 deletions(-) create mode 100644 src/tests/resolved_font_family.rs diff --git a/examples/text.rs b/examples/text.rs index f3ecb46..b36080a 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -266,11 +266,11 @@ impl<'a> App<'a> { } let upload_time = t_upload_start.elapsed(); - // println!( - // "rasterize: {} µs, load_texture: {} µs", - // raster_time.as_micros(), - // upload_time.as_micros() - // ); + println!( + "rasterize: {} µs, load_texture: {} µs", + raster_time.as_micros(), + upload_time.as_micros() + ); // TODO: cache shapes let text_shape_id = renderer.add_shape( diff --git a/src/buffer_utils.rs b/src/buffer_utils.rs index 8da2c3c..4e1de47 100644 --- a/src/buffer_utils.rs +++ b/src/buffer_utils.rs @@ -142,21 +142,6 @@ pub(crate) fn update_buffer( // Apply scale for shaping to device pixels buffer.set_size(font_system, Some(text_area_size.x * scale_factor), None); - // match font_family { - // FontFamily::Name(name) => { - // println!("Using font family: {:?}", name); - // } - // FontFamily::SansSerif => {} - // FontFamily::Serif => {} - // FontFamily::Monospace => {} - // FontFamily::Cursive => {} - // FontFamily::Fantasy => {} - // } - - if params.original_text() == "Filobus" { - println!("Font family: {:?}", font_family); - } - let mut attrs = Attrs::new() .color(font_color.into()) .family(font_family.to_fontdb_family()) @@ -200,6 +185,8 @@ pub(crate) fn update_buffer( } if buffer_measurement.x > text_area_size.x * scale_factor { + #[cfg(test)] + eprintln!("RELAYOUT: buffer_measurement.x={}, text_area_size.x * scale_factor={}", buffer_measurement.x, text_area_size.x * scale_factor); // If the buffer is smaller than the text area, we need to set the width to the text area // size to ensure that the text is centered. // After we've measured the buffer, we need to run layout() again to realign the lines diff --git a/src/font_family_query.rs b/src/font_family_query.rs index a8d611c..0b987e9 100644 --- a/src/font_family_query.rs +++ b/src/font_family_query.rs @@ -50,15 +50,28 @@ impl FontFamilyCache { } for family in query.split_families() { - let res = font_system.db().query(&fontdb::Query { - families: &[family.to_fontdb_family()], - weight: fontdb::Weight::NORMAL, - ..Default::default() - }); - if res.is_some() { - self.font_family_query_to_resolved_family_cache - .insert(query.clone(), family.clone()); - return family; + // Generic families (Monospace, SansSerif, Serif, etc.) should always be available + // Don't query the database for them, just return them directly + match &family { + FontFamily::Monospace | FontFamily::SansSerif | FontFamily::Serif + | FontFamily::Cursive | FontFamily::Fantasy => { + self.font_family_query_to_resolved_family_cache + .insert(query.clone(), family.clone()); + return family; + } + FontFamily::Name(_) => { + // For named fonts, query the database + let res = font_system.db().query(&fontdb::Query { + families: &[family.to_fontdb_family()], + weight: fontdb::Weight::NORMAL, + ..Default::default() + }); + if res.is_some() { + self.font_family_query_to_resolved_family_cache + .insert(query.clone(), family.clone()); + return family; + } + } } } diff --git a/src/state.rs b/src/state.rs index 1517cfe..5d4637d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -367,6 +367,25 @@ impl TextState { self.params.style() } + /// Returns the resolved font family after font matching has been performed. + /// + /// This may differ from the font family in the style if font substitution occurred. + /// + /// # Returns + /// A reference to the resolved font family + /// + /// # Examples + /// ``` + /// # use protextinator::TextState; + /// # use cosmic_text::FontSystem; + /// # let mut font_system = FontSystem::new(); + /// # let state = TextState::new_with_text("", &mut font_system, ()); + /// let resolved = state.resolved_font_family(); + /// ``` + pub fn resolved_font_family(&self) -> &FontFamily { + &self.resolved_font_family + } + /// Sets the visible area of the text buffer. This is going to be used to determine the buffer's /// viewport size and how much text is visible. /// diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 3f6993e..0d83b8e 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -9,6 +9,7 @@ mod caret_positioning; mod copy_selected_text; mod serialization; mod text_state; +mod resolved_font_family; fn mono_style_test() -> TextStyle { TextStyle { diff --git a/src/tests/resolved_font_family.rs b/src/tests/resolved_font_family.rs new file mode 100644 index 0000000..73db55d --- /dev/null +++ b/src/tests/resolved_font_family.rs @@ -0,0 +1,58 @@ +use crate::tests::mono_style_test; +use crate::{Point, TextContext, TextState}; +use crate::style::{FontFamily, TextStyle, FontColor, FontSize, HorizontalTextAlignment, LineHeight, TextWrap, VerticalTextAlignment, Weight}; +use cosmic_text::Color; + +#[test] +fn test_resolved_font_family_changes_to_monospace() { + let mut ctx = TextContext::default(); + let initial_text = "Hello World".to_string(); + + let mut text_state = TextState::new_with_text(initial_text, &mut ctx.font_system, ()); + + // Initially should have the default (SansSerif) + assert_eq!(text_state.resolved_font_family(), &FontFamily::SansSerif); + + // Set monospace style + text_state.set_style(&mono_style_test()); + text_state.set_outer_size(&Point::from((200.0, 25.0))); + + // After recalculate, should be Monospace + text_state.recalculate(&mut ctx); + + println!("Resolved font family: {:?}", text_state.resolved_font_family()); + assert_eq!(text_state.resolved_font_family(), &FontFamily::Monospace); +} + +#[test] +fn test_resolved_font_family_changes_to_serif() { + let mut ctx = TextContext::default(); + let initial_text = "Hello World".to_string(); + + let mut text_state = TextState::new_with_text(initial_text, &mut ctx.font_system, ()); + + // Initially should have the default (SansSerif) + assert_eq!(text_state.resolved_font_family(), &FontFamily::SansSerif); + + // Set serif style + let serif_style = TextStyle { + font_size: FontSize(14.0), + line_height: LineHeight(1.0), + font_color: FontColor(Color::rgb(0, 0, 0)), + horizontal_alignment: HorizontalTextAlignment::Start, + vertical_alignment: VerticalTextAlignment::Start, + wrap: Some(TextWrap::NoWrap), + font_family: FontFamily::Serif, + weight: Weight::NORMAL, + letter_spacing: None, + }; + + text_state.set_style(&serif_style); + text_state.set_outer_size(&Point::from((200.0, 25.0))); + + // After recalculate, should be Serif + text_state.recalculate(&mut ctx); + + println!("Resolved font family: {:?}", text_state.resolved_font_family()); + assert_eq!(text_state.resolved_font_family(), &FontFamily::Serif); +} From 60e68dcfbf5bbcb1214ecf231c2096d937c9730a Mon Sep 17 00:00:00 2001 From: Anton Suprunchuk Date: Sun, 18 Jan 2026 20:01:51 -0300 Subject: [PATCH 5/6] fix newline add (somewhat, now the scroll is a bit wierd, but I'll address that later) --- src/buffer_utils.rs | 6 +++++- src/font_family_query.rs | 7 +++++-- src/state.rs | 19 ++++++++++++++++-- src/tests/caret_positioning.rs | 11 ++++++----- src/tests/mod.rs | 2 +- src/tests/resolved_font_family.rs | 33 ++++++++++++++++++++----------- 6 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/buffer_utils.rs b/src/buffer_utils.rs index 4e1de47..4557425 100644 --- a/src/buffer_utils.rs +++ b/src/buffer_utils.rs @@ -186,7 +186,11 @@ pub(crate) fn update_buffer( if buffer_measurement.x > text_area_size.x * scale_factor { #[cfg(test)] - eprintln!("RELAYOUT: buffer_measurement.x={}, text_area_size.x * scale_factor={}", buffer_measurement.x, text_area_size.x * scale_factor); + eprintln!( + "RELAYOUT: buffer_measurement.x={}, text_area_size.x * scale_factor={}", + buffer_measurement.x, + text_area_size.x * scale_factor + ); // If the buffer is smaller than the text area, we need to set the width to the text area // size to ensure that the text is centered. // After we've measured the buffer, we need to run layout() again to realign the lines diff --git a/src/font_family_query.rs b/src/font_family_query.rs index 0b987e9..6fed5f6 100644 --- a/src/font_family_query.rs +++ b/src/font_family_query.rs @@ -53,8 +53,11 @@ impl FontFamilyCache { // Generic families (Monospace, SansSerif, Serif, etc.) should always be available // Don't query the database for them, just return them directly match &family { - FontFamily::Monospace | FontFamily::SansSerif | FontFamily::Serif - | FontFamily::Cursive | FontFamily::Fantasy => { + FontFamily::Monospace + | FontFamily::SansSerif + | FontFamily::Serif + | FontFamily::Cursive + | FontFamily::Fantasy => { self.font_family_query_to_resolved_family_cache .insert(query.clone(), family.clone()); return family; diff --git a/src/state.rs b/src/state.rs index 5d4637d..c0bfb8f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -542,8 +542,23 @@ impl TextState { } fn insert_char_at_cursor(&mut self, character: char, ctx: &mut TextContext) -> ActionResult { - self.params - .insert_char(self.cursor.byte_character_start, character); + let text = self.params.text_for_internal_use(); + let at_end = self.cursor.byte_character_start >= text.len(); + let ends_with_newline = text.ends_with('\n'); + + // cosmic_text quirk: when inserting a newline at the end of text that doesn't + // already end with a newline, we need to insert two newlines so the caret can + // be placed on the new line + if character == '\n' && at_end && !ends_with_newline { + self.params + .insert_char(self.cursor.byte_character_start, '\n'); + self.params + .insert_char(self.cursor.byte_character_start + 1, '\n'); + } else { + self.params + .insert_char(self.cursor.byte_character_start, character); + } + self.reshape_if_params_changed(ctx); self.move_cursor(ctx, Motion::Next); diff --git a/src/tests/caret_positioning.rs b/src/tests/caret_positioning.rs index a5a0bd5..318084d 100644 --- a/src/tests/caret_positioning.rs +++ b/src/tests/caret_positioning.rs @@ -218,17 +218,18 @@ pub fn test_insert_newline_at_end_of_text() { text_state.apply_action(&mut ctx, &Action::MoveCursorRight); } - // Verify cursor is at the end of the text + // Verify the cursor is at the end of the text assert_eq!(text_state.cursor_char_index(), Some(5)); assert_eq!(text_state.text_char_len(), 5); - // Try to delete a character at the end of the text - // This should panic due to the bug + // Insert a newline at the end of the text + // cosmic_text quirk: we need to insert two newlines at the end of text that doesn't + // already end with a newline, so the caret can be placed on the new line let result = text_state.apply_action(&mut ctx, &Action::InsertChar("\n".into())); // The following assertions should not be reached if the code panics assert!(matches!(result, ActionResult::TextChanged)); - assert_eq!(text_state.text_char_len(), 6); - assert_eq!(text_state.text(), "Hello\n"); + assert_eq!(text_state.text_char_len(), 7); + assert_eq!(text_state.text(), "Hello\n\n"); assert_eq!(text_state.cursor_char_index(), Some(6)); } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 0d83b8e..952c185 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -7,9 +7,9 @@ use cosmic_text::Color; mod byte_offset; mod caret_positioning; mod copy_selected_text; +mod resolved_font_family; mod serialization; mod text_state; -mod resolved_font_family; fn mono_style_test() -> TextStyle { TextStyle { diff --git a/src/tests/resolved_font_family.rs b/src/tests/resolved_font_family.rs index 73db55d..873dca0 100644 --- a/src/tests/resolved_font_family.rs +++ b/src/tests/resolved_font_family.rs @@ -1,6 +1,9 @@ +use crate::style::{ + FontColor, FontFamily, FontSize, HorizontalTextAlignment, LineHeight, TextStyle, TextWrap, + VerticalTextAlignment, Weight, +}; use crate::tests::mono_style_test; use crate::{Point, TextContext, TextState}; -use crate::style::{FontFamily, TextStyle, FontColor, FontSize, HorizontalTextAlignment, LineHeight, TextWrap, VerticalTextAlignment, Weight}; use cosmic_text::Color; #[test] @@ -9,18 +12,21 @@ fn test_resolved_font_family_changes_to_monospace() { let initial_text = "Hello World".to_string(); let mut text_state = TextState::new_with_text(initial_text, &mut ctx.font_system, ()); - + // Initially should have the default (SansSerif) assert_eq!(text_state.resolved_font_family(), &FontFamily::SansSerif); - + // Set monospace style text_state.set_style(&mono_style_test()); text_state.set_outer_size(&Point::from((200.0, 25.0))); - + // After recalculate, should be Monospace text_state.recalculate(&mut ctx); - - println!("Resolved font family: {:?}", text_state.resolved_font_family()); + + println!( + "Resolved font family: {:?}", + text_state.resolved_font_family() + ); assert_eq!(text_state.resolved_font_family(), &FontFamily::Monospace); } @@ -30,10 +36,10 @@ fn test_resolved_font_family_changes_to_serif() { let initial_text = "Hello World".to_string(); let mut text_state = TextState::new_with_text(initial_text, &mut ctx.font_system, ()); - + // Initially should have the default (SansSerif) assert_eq!(text_state.resolved_font_family(), &FontFamily::SansSerif); - + // Set serif style let serif_style = TextStyle { font_size: FontSize(14.0), @@ -46,13 +52,16 @@ fn test_resolved_font_family_changes_to_serif() { weight: Weight::NORMAL, letter_spacing: None, }; - + text_state.set_style(&serif_style); text_state.set_outer_size(&Point::from((200.0, 25.0))); - + // After recalculate, should be Serif text_state.recalculate(&mut ctx); - - println!("Resolved font family: {:?}", text_state.resolved_font_family()); + + println!( + "Resolved font family: {:?}", + text_state.resolved_font_family() + ); assert_eq!(text_state.resolved_font_family(), &FontFamily::Serif); } From b76cfa1e70bc013075a1b69d1159601de88aec23 Mon Sep 17 00:00:00 2001 From: Anton Suprunchuk Date: Sun, 18 Jan 2026 20:13:39 -0300 Subject: [PATCH 6/6] add some docs about font family --- src/style.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/style.rs b/src/style.rs index 8776b96..9067325 100644 --- a/src/style.rs +++ b/src/style.rs @@ -284,7 +284,8 @@ impl From for Color { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum FontFamily { /// A specific named font family. Have to be loaded into the font system with - /// [`crate::TextManager::load_fonts`]. + /// [`crate::TextManager::load_fonts`]. This can be a CSS-like font family query, i.e. + /// "Helvetica, 'Segoe UI', sans-serif", for example. Name(ArcCowStr), /// Generic sans-serif font family (e.g., Arial, Helvetica). SansSerif, @@ -323,6 +324,7 @@ impl FontFamily { /// /// let arial = FontFamily::new("Arial"); /// let custom_font = FontFamily::new("MyCustomFont".to_string()); + /// let css_like_font_family_query = FontFamily::new("Helvetica, 'Segoe UI', sans-serif"); /// ``` pub fn new(family: impl Into) -> Self { Self::Name(family.into()) @@ -511,7 +513,9 @@ pub struct TextStyle { pub vertical_alignment: VerticalTextAlignment, /// Text wrapping behavior. pub wrap: Option, - /// The font family to use for rendering. + /// The font family to use for rendering. Can be a generic family created with an enum, or + /// you can use a CSS-like font family query string to specify custom fonts: + /// `"Helvetica, 'Segoe UI', sans-serif".into()`. pub font_family: FontFamily, /// The font weight to use for rendering. pub weight: Weight,