1use std::{
19 cmp::Ordering,
20 collections::{
21 hash_map::{DefaultHasher, HashMap},
22 BTreeSet, HashSet,
23 },
24 hash::{Hash, Hasher},
25 mem::discriminant,
26 num::NonZeroUsize,
27 sync::{Arc, Mutex},
28};
29
30pub use azul_core::selection::{ContentIndex, GraphemeClusterId};
31use azul_core::{
32 dom::NodeId,
33 geom::{LogicalPosition, LogicalRect, LogicalSize},
34 resources::ImageRef,
35 selection::{CursorAffinity, SelectionRange, TextCursor},
36 ui_solver::GlyphInstance,
37};
38use azul_css::{
39 corety::LayoutDebugMessage, props::basic::ColorU, props::style::StyleBackgroundContent,
40};
41#[cfg(feature = "text_layout_hyphenation")]
42use hyphenation::{Hyphenator, Language as HyphenationLanguage, Load, Standard};
43use rust_fontconfig::{FcFontCache, FcPattern, FcStretch, FcWeight, FontId, PatternMatch, UnicodeRange};
44use smallvec::{smallvec, SmallVec};
45use unicode_bidi::{BidiInfo, Level, TextSource};
46use unicode_segmentation::UnicodeSegmentation;
47
48const FALLBACK_ASCENT_RATIO: f32 = 0.8;
53const FALLBACK_DESCENT_RATIO: f32 = 1.0 - FALLBACK_ASCENT_RATIO;
54
55const DEFAULT_STRUT_ASCENT: f32 = 12.8;
60const DEFAULT_STRUT_DESCENT: f32 = 3.2;
62
63const DEFAULT_X_HEIGHT: f32 = 8.0;
65const DEFAULT_CH_WIDTH: f32 = 8.0;
67
68const SPACE_WIDTH_RATIO: f32 = 0.5;
70
71const SUBSCRIPT_OFFSET_RATIO: f32 = 0.3;
73const SUPERSCRIPT_OFFSET_RATIO: f32 = 0.4;
75
76const RUBY_ANNOTATION_FONT_SCALE: f32 = 0.5;
80
81fn ruby_reserved_box(
87 base_width: f32,
88 annotation_width: f32,
89 base_line_height: f32,
90 annotation_line_height: f32,
91) -> (f32, f32) {
92 (
93 base_width.max(annotation_width),
94 base_line_height + annotation_line_height,
95 )
96}
97
98pub type ShapedGlyphVec = SmallVec<[ShapedGlyph; 1]>;
106
107#[derive(Debug, Clone, Copy)]
114#[derive(Default)]
115pub enum LineHeight {
116 #[default]
118 Normal,
119 Px(f32),
121}
122
123
124impl LineHeight {
125 #[must_use] pub fn resolve(&self, font_size_px: f32, ascent: f32, descent: f32, line_gap: f32, units_per_em: u16) -> f32 {
130 match self {
131 Self::Px(px) => *px,
132 Self::Normal => {
133 if units_per_em == 0 {
134 return font_size_px * 1.2; }
136 let scale = font_size_px / f32::from(units_per_em);
137 (ascent - descent + line_gap) * scale
138 }
139 }
140 }
141
142 #[must_use] pub fn resolve_with_metrics(&self, font_size_px: f32, metrics: &LayoutFontMetrics) -> f32 {
144 self.resolve(font_size_px, metrics.ascent, metrics.descent, metrics.line_gap, metrics.units_per_em)
145 }
146}
147
148impl PartialEq for LineHeight {
149 fn eq(&self, other: &Self) -> bool {
150 match (self, other) {
151 (Self::Normal, Self::Normal) => true,
152 (Self::Px(a), Self::Px(b)) => a.to_bits() == b.to_bits(),
153 _ => false,
154 }
155 }
156}
157
158impl Eq for LineHeight {}
159
160impl Hash for LineHeight {
161 fn hash<H: Hasher>(&self, state: &mut H) {
162 discriminant(self).hash(state);
163 if let Self::Px(v) = self {
164 v.to_bits().hash(state);
165 }
166 }
167}
168
169#[cfg(not(feature = "text_layout_hyphenation"))]
171pub struct Standard;
172
173#[cfg(not(feature = "text_layout_hyphenation"))]
174impl Standard {
175 pub fn hyphenate<'a>(&'a self, _word: &'a str) -> StubHyphenationBreaks {
177 StubHyphenationBreaks { breaks: Vec::new() }
178 }
179}
180
181#[cfg(not(feature = "text_layout_hyphenation"))]
183pub struct StubHyphenationBreaks {
184 pub breaks: Vec<usize>,
185}
186
187use crate::text3::script::{script_to_language, Language, Script};
189
190#[derive(Debug, Clone, Copy, PartialEq)]
201pub enum AvailableSpace {
202 Definite(f32),
206 MinContent,
208 MaxContent,
211}
212
213impl Default for AvailableSpace {
214 fn default() -> Self {
217 Self::MaxContent
218 }
219}
220
221impl AvailableSpace {
222 #[must_use] pub const fn is_definite(&self) -> bool {
224 matches!(self, Self::Definite(_))
225 }
226
227 #[must_use] pub const fn is_indefinite(&self) -> bool {
229 !self.is_definite()
230 }
231
232 #[must_use] pub const fn unwrap_or(self, fallback: f32) -> f32 {
234 match self {
235 Self::Definite(v) => v,
236 _ => fallback,
237 }
238 }
239
240 #[allow(clippy::match_same_arms)] #[must_use] pub fn to_f32_for_layout(self) -> f32 {
247 match self {
248 Self::Definite(v) => v,
249 Self::MinContent => f32::MAX / 2.0,
250 Self::MaxContent => f32::MAX / 2.0,
251 }
252 }
253
254 #[must_use] pub fn from_f32(value: f32) -> Self {
264 if value.is_infinite() || value >= f32::MAX / 2.0 {
265 Self::MaxContent
267 } else if value <= 0.0 {
268 Self::MinContent
270 } else {
271 Self::Definite(value)
272 }
273 }
274}
275
276impl Hash for AvailableSpace {
277 fn hash<H: Hasher>(&self, state: &mut H) {
278 discriminant(self).hash(state);
279 if let Self::Definite(v) = self {
280 let normalized = if *v == 0.0 { 0.0f32 } else { *v };
288 normalized.to_bits().hash(state);
289 }
290 }
291}
292
293pub use crate::font_traits::{ParsedFontTrait, ShallowClone};
295
296#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
300pub struct FontChainKey {
301 pub font_families: Vec<String>,
302 pub weight: FcWeight,
303 pub italic: bool,
304 pub oblique: bool,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
313pub enum FontChainKeyOrRef {
314 Chain(FontChainKey),
316 Ref(usize),
318}
319
320impl FontChainKeyOrRef {
321 #[must_use] pub fn from_font_stack(font_stack: &FontStack) -> Self {
323 match font_stack {
324 FontStack::Stack(selectors) => Self::Chain(FontChainKey::from_selectors(selectors)),
325 FontStack::Ref(font_ref) => Self::Ref(font_ref.parsed as usize),
326 }
327 }
328
329 #[must_use] pub const fn is_ref(&self) -> bool {
331 matches!(self, Self::Ref(_))
332 }
333
334 #[must_use] pub const fn as_ref_ptr(&self) -> Option<usize> {
336 match self {
337 Self::Ref(ptr) => Some(*ptr),
338 Self::Chain(_) => None,
339 }
340 }
341
342 #[must_use] pub const fn as_chain(&self) -> Option<&FontChainKey> {
344 match self {
345 Self::Chain(key) => Some(key),
346 Self::Ref(_) => None,
347 }
348 }
349}
350
351impl FontChainKey {
352 #[must_use] pub fn from_selectors(font_stack: &[FontSelector]) -> Self {
354 let mut font_families: Vec<String> = Vec::new();
362 for sel in font_stack {
363 if sel.family.is_empty() || font_families.contains(&sel.family) {
364 continue;
365 }
366 font_families.push(sel.family.clone());
367 }
368
369 let font_families = if font_families.is_empty() {
370 vec!["serif".to_string()]
371 } else {
372 font_families
373 };
374
375 let weight = font_stack
376 .first()
377 .map_or(FcWeight::Normal, |s| s.weight);
378 let is_italic = font_stack
379 .first()
380 .is_some_and(|s| s.style == FontStyle::Italic);
381 let is_oblique = font_stack
382 .first()
383 .is_some_and(|s| s.style == FontStyle::Oblique);
384
385 Self {
386 font_families,
387 weight,
388 italic: is_italic,
389 oblique: is_oblique,
390 }
391 }
392}
393
394#[derive(Debug, Clone)]
401pub struct LoadedFonts<T> {
402 pub fonts: HashMap<FontId, T>,
404 hash_to_id: HashMap<u64, FontId>,
406}
407
408impl<T: ParsedFontTrait> LoadedFonts<T> {
409 #[must_use] pub fn new() -> Self {
410 Self {
411 fonts: HashMap::new(),
412 hash_to_id: HashMap::new(),
413 }
414 }
415
416 pub fn insert(&mut self, font_id: FontId, font: T) {
418 let hash = font.get_hash();
419 self.hash_to_id.insert(hash, font_id);
420 self.fonts.insert(font_id, font);
421 }
422
423 #[must_use] pub fn get(&self, font_id: &FontId) -> Option<&T> {
425 self.fonts.get(font_id)
426 }
427
428 #[must_use] pub fn get_by_hash(&self, hash: u64) -> Option<&T> {
430 self.hash_to_id.get(&hash).and_then(|id| self.fonts.get(id))
431 }
432
433 #[must_use] pub fn get_font_id_by_hash(&self, hash: u64) -> Option<&FontId> {
435 self.hash_to_id.get(&hash)
436 }
437
438 #[must_use] pub fn contains_key(&self, font_id: &FontId) -> bool {
440 self.fonts.contains_key(font_id)
441 }
442
443 #[must_use] pub fn contains_hash(&self, hash: u64) -> bool {
445 self.hash_to_id.contains_key(&hash)
446 }
447
448 pub fn iter(&self) -> impl Iterator<Item = (&FontId, &T)> {
450 self.fonts.iter()
451 }
452
453 #[must_use] pub fn len(&self) -> usize {
455 self.fonts.len()
456 }
457
458 #[must_use] pub fn is_empty(&self) -> bool {
460 self.fonts.is_empty()
461 }
462}
463
464impl<T: ParsedFontTrait> Default for LoadedFonts<T> {
465 fn default() -> Self {
466 Self::new()
467 }
468}
469
470impl<T: ParsedFontTrait> FromIterator<(FontId, T)> for LoadedFonts<T> {
471 fn from_iter<I: IntoIterator<Item = (FontId, T)>>(iter: I) -> Self {
472 let mut loaded = Self::new();
473 for (id, font) in iter {
474 loaded.insert(id, font);
475 }
476 loaded
477 }
478}
479
480#[derive(Debug, Clone)]
485pub enum FontOrRef<T> {
486 Font(T),
488 Ref(azul_css::props::basic::FontRef),
490}
491
492impl<T: ParsedFontTrait> ShallowClone for FontOrRef<T> {
493 fn shallow_clone(&self) -> Self {
494 match self {
495 Self::Font(f) => Self::Font(f.shallow_clone()),
496 Self::Ref(r) => Self::Ref(r.clone()),
497 }
498 }
499}
500
501impl<T: ParsedFontTrait> ParsedFontTrait for FontOrRef<T> {
502 fn shape_text(
503 &self,
504 text: &str,
505 script: Script,
506 language: Language,
507 direction: BidiDirection,
508 style: &StyleProperties,
509 ) -> Result<Vec<Glyph>, LayoutError> {
510 match self {
511 Self::Font(f) => f.shape_text(text, script, language, direction, style),
512 Self::Ref(r) => r.shape_text(text, script, language, direction, style),
513 }
514 }
515
516 fn get_hash(&self) -> u64 {
517 match self {
518 Self::Font(f) => f.get_hash(),
519 Self::Ref(r) => r.get_hash(),
520 }
521 }
522
523 fn get_glyph_size(&self, glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
524 match self {
525 Self::Font(f) => f.get_glyph_size(glyph_id, font_size),
526 Self::Ref(r) => r.get_glyph_size(glyph_id, font_size),
527 }
528 }
529
530 fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
531 match self {
532 Self::Font(f) => f.get_hyphen_glyph_and_advance(font_size),
533 Self::Ref(r) => r.get_hyphen_glyph_and_advance(font_size),
534 }
535 }
536
537 fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
538 match self {
539 Self::Font(f) => f.get_kashida_glyph_and_advance(font_size),
540 Self::Ref(r) => r.get_kashida_glyph_and_advance(font_size),
541 }
542 }
543
544 fn has_glyph(&self, codepoint: u32) -> bool {
545 match self {
546 Self::Font(f) => f.has_glyph(codepoint),
547 Self::Ref(r) => r.has_glyph(codepoint),
548 }
549 }
550
551 fn get_vertical_metrics(&self, glyph_id: u16) -> Option<VerticalMetrics> {
552 match self {
553 Self::Font(f) => f.get_vertical_metrics(glyph_id),
554 Self::Ref(r) => r.get_vertical_metrics(glyph_id),
555 }
556 }
557
558 fn get_font_metrics(&self) -> LayoutFontMetrics {
559 match self {
560 Self::Font(f) => f.get_font_metrics(),
561 Self::Ref(r) => r.get_font_metrics(),
562 }
563 }
564
565 fn num_glyphs(&self) -> u16 {
566 match self {
567 Self::Font(f) => f.num_glyphs(),
568 Self::Ref(r) => r.num_glyphs(),
569 }
570 }
571
572 fn get_space_width(&self) -> Option<usize> {
573 match self {
574 Self::Font(f) => f.get_space_width(),
575 Self::Ref(r) => r.get_space_width(),
576 }
577 }
578}
579
580#[derive(Debug, Clone)]
597pub struct FontContext {
598 pub fc_cache: FcFontCache,
603 pub parsed_fonts: Arc<Mutex<HashMap<FontId, azul_css::props::basic::FontRef>>>,
604 pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
605 pub embedded_fonts: HashMap<u64, azul_css::props::basic::FontRef>,
606 pub font_hash_to_families: HashMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
609 pub registry: Option<Arc<rust_fontconfig::registry::FcFontRegistry>>,
615}
616
617impl FontContext {
618 #[must_use] pub fn from_fc_cache(fc_cache: FcFontCache) -> Self {
627 Self {
628 fc_cache,
629 parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
630 font_chain_cache: HashMap::new(),
631 embedded_fonts: HashMap::new(),
632 font_hash_to_families: HashMap::new(),
633 registry: None,
634 }
635 }
636
637 pub fn from_registry(
649 registry: Arc<rust_fontconfig::registry::FcFontRegistry>,
650 ) -> Self {
651 let fc_cache = registry.shared_cache();
652 Self {
653 fc_cache,
654 parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
655 font_chain_cache: HashMap::new(),
656 embedded_fonts: HashMap::new(),
657 font_hash_to_families: HashMap::new(),
658 registry: Some(registry),
659 }
660 }
661
662 pub fn pre_resolve_chains_for_dom(
671 &mut self,
672 styled_dom: &azul_core::styled_dom::StyledDom,
673 platform: &azul_css::system::Platform,
674 ) {
675 use crate::solver3::getters::{
676 collect_font_stacks_from_styled_dom, collect_used_codepoints,
677 prune_chain_to_used_chars, resolve_font_chains, scripts_present_in_styled_dom,
678 };
679 let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
680 let scripts = scripts_present_in_styled_dom(styled_dom);
681 let mut chains = resolve_font_chains(&collected, &self.fc_cache, Some(&scripts));
682 let used_chars = collect_used_codepoints(styled_dom);
684 for chain in chains.chains.values_mut() {
685 prune_chain_to_used_chars(chain, &used_chars);
686 }
687 for chain in chains.chains.values_mut() {
693 let total = chain.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>()
694 + chain.unicode_fallbacks.len();
695 if total == 0 {
696 if let Some((pattern, id)) = self.fc_cache.list().first() {
697 chain.unicode_fallbacks.push(rust_fontconfig::FontMatch {
698 id: *id,
699 unicode_ranges: pattern.unicode_ranges.clone(),
700 fallbacks: Vec::new(),
701 });
702 }
703 }
704 }
705 self.font_chain_cache = chains.into_fontconfig_chains();
706 }
707
708 pub fn load_fonts_for_chains(&self) {
718 use crate::solver3::getters::ResolvedFontChains;
719 use crate::text3::default::PathLoader;
720
721 let chains_map: HashMap<FontChainKeyOrRef, _> = self
722 .font_chain_cache
723 .iter()
724 .map(|(k, v)| (FontChainKeyOrRef::Chain(k.clone()), v.clone()))
725 .collect();
726 let resolved = ResolvedFontChains {
727 chains: chains_map,
728 ..Default::default()
729 };
730
731 let Ok(manager) = FontManager::<azul_css::props::basic::FontRef>::from_arc_shared(
735 self.fc_cache.clone(),
736 self.parsed_fonts.clone(),
737 ) else {
738 return;
739 };
740 let loader = PathLoader::new();
741 let _failed = manager
742 .load_missing_for_chains(&resolved, |bytes, idx| loader.load_font_shared(bytes, idx));
743 }
744
745 #[must_use] pub fn to_font_manager(&self) -> FontManager<azul_css::props::basic::FontRef> {
749 let mut fm = FontManager {
750 fc_cache: self.fc_cache.clone(),
751 parsed_fonts: self.parsed_fonts.clone(),
752 font_chain_cache: self.font_chain_cache.clone(),
753 embedded_fonts: Mutex::new(self.embedded_fonts.clone()),
754 font_hash_to_families: self.font_hash_to_families.clone(),
755 registry: self.registry.clone(),
756 last_resolved_font_stacks_sig: None,
757 memory_families: HashMap::new(),
758 vf_bake_cache: HashMap::new(),
759 };
760 fm.register_builtin_mock_fonts();
762 fm
763 }
764}
765
766#[derive(Debug, Clone, Copy, PartialEq, Eq)]
779pub enum MemoryFontTier {
780 Primary,
782 Fallback,
785}
786
787#[derive(Debug, Clone)]
797pub struct MemoryFace {
798 pub tier: MemoryFontTier,
800 pub font_match: rust_fontconfig::FontMatch,
802 pub weight: FcWeight,
805 pub italic: bool,
807 pub oblique: bool,
809 pub stretch: FcStretch,
811 pub weight_axis: Option<(f32, f32)>,
814}
815
816#[derive(Debug, Clone, Copy)]
819struct FaceStyle {
820 weight: FcWeight,
821 italic: bool,
822 oblique: bool,
823 stretch: FcStretch,
824 weight_axis: Option<(f32, f32)>,
825}
826
827impl Default for FaceStyle {
828 fn default() -> Self {
829 Self {
830 weight: FcWeight::Normal,
831 italic: false,
832 oblique: false,
833 stretch: FcStretch::Normal,
834 weight_axis: None,
835 }
836 }
837}
838
839fn parse_face_style(bytes: &[u8], family: &str) -> FaceStyle {
844 let Some(faces) = rust_fontconfig::FcParseFontBytes(bytes, family) else {
845 return FaceStyle::default();
846 };
847 let Some((pat, _)) = faces.into_iter().next() else {
848 return FaceStyle::default();
849 };
850 FaceStyle {
851 weight: pat.weight,
852 italic: pat.italic == PatternMatch::True,
853 oblique: pat.oblique == PatternMatch::True,
854 stretch: pat.stretch,
855 weight_axis: None,
856 }
857}
858
859#[derive(Debug)]
860pub struct FontManager<T> {
861 pub fc_cache: FcFontCache,
866 pub parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
870 pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
873 pub embedded_fonts: Mutex<HashMap<u64, azul_css::props::basic::FontRef>>,
876 pub font_hash_to_families: HashMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
880 pub registry: Option<Arc<rust_fontconfig::registry::FcFontRegistry>>,
887 pub last_resolved_font_stacks_sig: Option<u64>,
894 pub memory_families: HashMap<String, Vec<MemoryFace>>,
910 vf_bake_cache: HashMap<u64, Vec<(FontId, FaceStyle)>>,
915}
916
917impl<T: ParsedFontTrait> FontManager<T> {
918 pub fn new(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
922 let mut fm = Self {
923 fc_cache,
924 parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
925 font_chain_cache: HashMap::new(),
926 embedded_fonts: Mutex::new(HashMap::new()),
927 font_hash_to_families: HashMap::new(),
928 registry: None,
929 last_resolved_font_stacks_sig: None,
930 memory_families: HashMap::new(),
931 vf_bake_cache: HashMap::new(),
932 };
933 fm.register_builtin_mock_fonts();
934 Ok(fm)
935 }
936
937 pub fn register_named_font(
955 &mut self,
956 family: &str,
957 bytes: &[u8],
958 coverage: Vec<UnicodeRange>,
959 ) -> FontId {
960 self.register_named_font_in_tier(family, bytes, coverage, MemoryFontTier::Primary)
961 }
962
963 pub fn register_named_font_in_tier(
973 &mut self,
974 family: &str,
975 bytes: &[u8],
976 coverage: Vec<UnicodeRange>,
977 tier: MemoryFontTier,
978 ) -> FontId {
979 let norm = rust_fontconfig::utils::normalize_family_name(family);
980
981 if let Some((min, def, max)) = crate::font::parsed::read_wght_axis(bytes, 0) {
988 if let Some(id) =
989 self.register_variable_instances(&norm, family, bytes, &coverage, min, def, max, tier)
990 {
991 return id;
992 }
993 }
994
995 let style = parse_face_style(bytes, family);
1001
1002 let mut existing: Vec<(FontId, Vec<UnicodeRange>)> = Vec::new();
1009 self.fc_cache.for_each_pattern(|pattern, id| {
1010 let fam_hit = pattern
1011 .family
1012 .as_deref()
1013 .is_some_and(|f| rust_fontconfig::utils::normalize_family_name(f) == norm);
1014 let style_hit = pattern.weight == style.weight
1015 && (pattern.italic == PatternMatch::True) == style.italic
1016 && (pattern.oblique == PatternMatch::True) == style.oblique;
1017 if fam_hit && style_hit {
1018 existing.push((*id, pattern.unicode_ranges.clone()));
1019 }
1020 });
1021 let id = if let Some((id, ranges)) = existing
1022 .into_iter()
1023 .find(|(id, _)| self.fc_cache.is_memory_font(id))
1024 {
1025 self.index_memory_face(&norm, id, ranges, &style, tier);
1026 id
1027 } else {
1028 let pattern = rust_fontconfig::FcPattern {
1029 name: Some(family.to_string()),
1030 family: Some(family.to_string()),
1031 italic: if style.italic { PatternMatch::True } else { PatternMatch::False },
1032 oblique: if style.oblique { PatternMatch::True } else { PatternMatch::False },
1033 bold: if style.weight >= FcWeight::Bold { PatternMatch::True } else { PatternMatch::False },
1034 weight: style.weight,
1035 stretch: style.stretch,
1036 unicode_ranges: coverage.clone(),
1037 ..Default::default()
1038 };
1039 let id = FontId::new();
1040 self.fc_cache.with_memory_font_with_id(
1041 id,
1042 pattern,
1043 rust_fontconfig::FcFont {
1044 bytes: bytes.to_vec(),
1045 font_index: 0,
1046 id: family.to_string(),
1047 },
1048 );
1049 self.index_memory_face(&norm, id, coverage, &style, tier);
1050 id
1051 };
1052 id
1053 }
1054
1055 fn index_memory_face(
1058 &mut self,
1059 norm: &str,
1060 id: FontId,
1061 unicode_ranges: Vec<UnicodeRange>,
1062 style: &FaceStyle,
1063 tier: MemoryFontTier,
1064 ) {
1065 let face = MemoryFace {
1066 tier,
1067 font_match: rust_fontconfig::FontMatch {
1068 id,
1069 unicode_ranges,
1070 fallbacks: Vec::new(),
1071 },
1072 weight: style.weight,
1073 italic: style.italic,
1074 oblique: style.oblique,
1075 stretch: style.stretch,
1076 weight_axis: style.weight_axis,
1077 };
1078 let faces = self.memory_families.entry(norm.to_string()).or_default();
1079 if let Some(slot) = faces.iter_mut().find(|f| f.font_match.id == id) {
1080 *slot = face;
1081 } else {
1082 faces.push(face);
1083 }
1084 }
1085
1086 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1097 fn register_variable_instances(
1098 &mut self,
1099 norm: &str,
1100 family: &str,
1101 bytes: &[u8],
1102 coverage: &[UnicodeRange],
1103 min: f32,
1104 def: f32,
1105 max: f32,
1106 tier: MemoryFontTier,
1107 ) -> Option<FontId> {
1108 let base = parse_face_style(bytes, family);
1109 let hash = {
1110 use core::hash::Hasher;
1111 let mut h = DefaultHasher::new();
1112 h.write(bytes);
1113 h.finish()
1114 };
1115 let def_bucket = FcWeight::from_u16(def.round().clamp(1.0, 1000.0) as u16);
1116
1117 if let Some(cached) = self.vf_bake_cache.get(&hash).cloned() {
1119 for (id, style) in &cached {
1120 self.index_memory_face(norm, *id, coverage.to_vec(), style, tier);
1121 }
1122 return cached
1123 .iter()
1124 .find(|(_, s)| s.weight == def_bucket)
1125 .or_else(|| cached.first())
1126 .map(|(id, _)| *id);
1127 }
1128
1129 let lo = min.round().clamp(1.0, 1000.0) as u16;
1130 let hi = max.round().clamp(1.0, 1000.0) as u16;
1131 let mut baked: Vec<(FontId, FaceStyle)> = Vec::new();
1132 for w in [100u16, 200, 300, 400, 500, 600, 700, 800, 900] {
1133 if w < lo || w > hi {
1134 continue;
1135 }
1136 let Some(inst_bytes) = crate::font::parsed::bake_weight_instance(bytes, 0, f32::from(w))
1137 else {
1138 continue;
1139 };
1140 let style = FaceStyle {
1141 weight: FcWeight::from_u16(w),
1142 italic: base.italic,
1143 oblique: base.oblique,
1144 stretch: base.stretch,
1145 weight_axis: None,
1146 };
1147 let pattern = rust_fontconfig::FcPattern {
1148 name: Some(family.to_string()),
1149 family: Some(family.to_string()),
1150 italic: if style.italic { PatternMatch::True } else { PatternMatch::False },
1151 oblique: if style.oblique { PatternMatch::True } else { PatternMatch::False },
1152 bold: if style.weight >= FcWeight::Bold { PatternMatch::True } else { PatternMatch::False },
1153 weight: style.weight,
1154 stretch: style.stretch,
1155 unicode_ranges: coverage.to_vec(),
1156 ..Default::default()
1157 };
1158 let id = FontId::new();
1159 self.fc_cache.with_memory_font_with_id(
1160 id,
1161 pattern,
1162 rust_fontconfig::FcFont {
1163 bytes: inst_bytes,
1164 font_index: 0,
1165 id: family.to_string(),
1166 },
1167 );
1168 self.index_memory_face(norm, id, coverage.to_vec(), &style, tier);
1169 baked.push((id, style));
1170 }
1171
1172 if baked.is_empty() {
1173 return None;
1174 }
1175 let default_id = baked
1176 .iter()
1177 .find(|(_, s)| s.weight == def_bucket)
1178 .or_else(|| baked.first())
1179 .map(|(id, _)| *id)
1180 .unwrap();
1181 self.vf_bake_cache.insert(hash, baked);
1182 Some(default_id)
1183 }
1184
1185 pub fn register_builtin_mock_fonts(&mut self) {
1191 for (family, bytes) in crate::text3::mock_fonts::BUILTIN_MOCK_FONTS {
1192 self.register_named_font(
1193 family,
1194 bytes,
1195 crate::text3::mock_fonts::mock_font_ranges(),
1196 );
1197 }
1198 }
1199
1200 pub fn from_shared(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
1210 Self::new(fc_cache)
1211 }
1212
1213 pub fn from_arc_shared(
1222 fc_cache: FcFontCache,
1223 parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
1224 ) -> Result<Self, LayoutError> {
1225 let mut fm = Self {
1226 fc_cache,
1227 parsed_fonts,
1228 font_chain_cache: HashMap::new(),
1229 embedded_fonts: Mutex::new(HashMap::new()),
1230 font_hash_to_families: HashMap::new(),
1231 registry: None,
1232 last_resolved_font_stacks_sig: None,
1233 memory_families: HashMap::new(),
1234 vf_bake_cache: HashMap::new(),
1235 };
1236 fm.register_builtin_mock_fonts();
1237 Ok(fm)
1238 }
1239
1240 #[must_use]
1244 pub fn with_registry(
1245 mut self,
1246 registry: Arc<rust_fontconfig::registry::FcFontRegistry>,
1247 ) -> Self {
1248 self.registry = Some(registry);
1249 self
1250 }
1251
1252 pub fn shared_parsed_fonts(&self) -> Arc<Mutex<HashMap<FontId, T>>> {
1257 Arc::clone(&self.parsed_fonts)
1258 }
1259
1260 pub fn set_font_chain_cache(
1265 &mut self,
1266 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
1267 ) {
1268 self.font_chain_cache = chains;
1269 self.last_resolved_font_stacks_sig = None;
1270 }
1271
1272 pub fn set_font_chain_cache_with_sig(
1278 &mut self,
1279 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
1280 sig: Option<u64>,
1281 ) {
1282 self.font_chain_cache = chains;
1286 self.last_resolved_font_stacks_sig = sig;
1287 }
1288
1289 pub fn merge_font_chain_cache(
1293 &mut self,
1294 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
1295 ) {
1296 self.font_chain_cache.extend(chains);
1297 }
1298
1299 pub const fn get_font_chain_cache(
1301 &self,
1302 ) -> &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain> {
1303 &self.font_chain_cache
1304 }
1305
1306 pub fn get_embedded_font_by_hash(&self, font_hash: u64) -> Option<azul_css::props::basic::FontRef> {
1312 let embedded = self.embedded_fonts.lock().unwrap();
1313 embedded.get(&font_hash).cloned()
1314 }
1315
1316 pub fn get_font_by_hash(&self, font_hash: u64) -> Option<T> {
1322 let parsed = self.parsed_fonts.lock().unwrap();
1323 let found = parsed
1325 .iter()
1326 .find(|(_, font)| font.get_hash() == font_hash)
1327 .map(|(_, font)| font.clone());
1328 drop(parsed);
1329 found
1330 }
1331
1332 #[must_use]
1352 pub fn resolve_font_by_hash(&self, font_hash: u64) -> Option<azul_css::props::basic::FontRef>
1353 where
1354 T: Into<azul_css::props::basic::FontRef> + Clone,
1355 {
1356 if let Some(embedded) = self.get_embedded_font_by_hash(font_hash) {
1357 return Some(embedded);
1358 }
1359 self.get_font_by_hash(font_hash).map(Into::into)
1360 }
1361
1362 pub fn register_embedded_font(&self, font_ref: &azul_css::props::basic::FontRef) {
1368 let hash = font_ref.get_hash();
1369 let mut embedded = self.embedded_fonts.lock().unwrap();
1370 embedded.insert(hash, font_ref.clone());
1371 }
1372
1373 pub fn get_loaded_fonts(&self) -> LoadedFonts<T> {
1383 let parsed = self.parsed_fonts.lock().unwrap();
1384 parsed
1385 .iter()
1386 .map(|(id, font)| (*id, font.shallow_clone()))
1387 .collect()
1388 }
1389
1390 pub fn get_loaded_font_ids(&self) -> HashSet<FontId> {
1398 let parsed = self.parsed_fonts.lock().unwrap();
1399 if parsed.is_empty() {
1404 return HashSet::new();
1405 }
1406 unsafe { crate::az_mark(0x60788, 0xA1) };
1407 let out = parsed.keys().copied().collect();
1408 drop(parsed);
1409 unsafe { crate::az_mark(0x6078C, 0xA2) };
1410 out
1411 }
1412
1413 pub fn insert_font(&self, font_id: FontId, font: T) -> Option<T> {
1420 let mut parsed = self.parsed_fonts.lock().unwrap();
1421 parsed.insert(font_id, font)
1422 }
1423
1424 pub fn insert_fonts(&self, fonts: impl IntoIterator<Item = (FontId, T)>) {
1432 let mut parsed = self.parsed_fonts.lock().unwrap();
1433 for (font_id, font) in fonts {
1434 parsed.insert(font_id, font);
1435 }
1436 }
1437
1438 pub fn load_missing_for_chains<F>(
1450 &self,
1451 chains: &crate::solver3::getters::ResolvedFontChains,
1452 load_fn: F,
1453 ) -> Vec<(FontId, String)>
1454 where
1455 F: Fn(Arc<rust_fontconfig::FontBytes>, usize) -> Result<T, LayoutError>,
1456 {
1457 use crate::solver3::getters::{
1458 collect_font_ids_from_chains, compute_fonts_to_load, load_fonts_from_disk,
1459 };
1460 let required = collect_font_ids_from_chains(chains);
1461 let already = self.get_loaded_font_ids();
1462 let to_load = compute_fonts_to_load(&required, &already);
1463 if to_load.is_empty() {
1464 return Vec::new();
1465 }
1466 let result = load_fonts_from_disk(&to_load, &self.fc_cache, load_fn);
1467 self.insert_fonts(result.loaded);
1468 result.failed
1469 }
1470
1471 pub fn replace_fc_cache(&mut self, fc_cache: FcFontCache) {
1481 self.fc_cache = fc_cache;
1482 self.drop_dangling_memory_faces();
1483 self.register_builtin_mock_fonts();
1484 }
1485
1486 fn drop_dangling_memory_faces(&mut self) {
1507 let fc_cache = &self.fc_cache;
1508 self.memory_families.retain(|_, faces| {
1509 faces.retain(|f| fc_cache.is_memory_font(&f.font_match.id));
1510 !faces.is_empty()
1511 });
1512 self.vf_bake_cache
1515 .retain(|_, baked| baked.iter().all(|(id, _)| fc_cache.is_memory_font(id)));
1516 }
1517
1518 pub fn remove_font(&self, font_id: &FontId) -> Option<T> {
1525 let mut parsed = self.parsed_fonts.lock().unwrap();
1526 parsed.remove(font_id)
1527 }
1528
1529 pub fn garbage_collect_fonts(
1550 &mut self,
1551 keep_ids: &HashSet<FontId>,
1552 keep_hashes: &HashSet<u64>,
1553 ) -> usize {
1554 let evicted = {
1555 let mut parsed = self.parsed_fonts.lock().unwrap();
1556 let before = parsed.len();
1557 parsed.retain(|id, _| keep_ids.contains(id));
1558 before.saturating_sub(parsed.len())
1559 };
1560 self.font_hash_to_families
1561 .retain(|h, _| keep_hashes.contains(h));
1562 evicted
1563 }
1564}
1565
1566#[derive(Debug, thiserror::Error)]
1572#[repr(C, u8)]
1573pub enum LayoutError {
1574 #[error("Bidi analysis failed: {0}")]
1575 BidiError(String),
1576 #[error("Shaping failed: {0}")]
1577 ShapingError(String),
1578 #[error("Font not found: {0:?}")]
1579 FontNotFound(FontSelector),
1580 #[error("Invalid text input: {0}")]
1581 InvalidText(String),
1582 #[error("Hyphenation failed: {0}")]
1583 HyphenationError(String),
1584}
1585
1586#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1588pub enum TextBoundary {
1589 Top,
1591 Bottom,
1593 Start,
1595 End,
1597}
1598
1599#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1601pub(crate) struct CursorBoundsError {
1602 pub(crate) boundary: TextBoundary,
1603 pub(crate) cursor: TextCursor,
1604}
1605
1606#[derive(Debug, Clone)]
1677pub struct UnifiedConstraints {
1678 pub shape_boundaries: Vec<ShapeBoundary>,
1680 pub shape_exclusions: Vec<ShapeBoundary>,
1681
1682 pub available_width: AvailableSpace,
1684 pub available_height: Option<f32>,
1685
1686 pub writing_mode: Option<WritingMode>,
1688 pub direction: Option<BidiDirection>,
1691 pub text_orientation: TextOrientation,
1692 pub text_align: TextAlign,
1693 pub text_justify: JustifyContent,
1694 pub line_height: LineHeight,
1696 pub vertical_align: VerticalAlign,
1697 pub strut_ascent: f32,
1699 pub strut_descent: f32,
1700 pub strut_x_height: f32,
1702
1703 pub ch_width: f32,
1706
1707 pub overflow: OverflowBehavior,
1709 pub segment_alignment: SegmentAlignment,
1710
1711 pub text_combine_upright: Option<TextCombineUpright>,
1713 pub exclusion_margin: f32,
1714 pub hyphenation: Hyphens,
1715 pub hyphenation_language: Option<Language>,
1716 pub text_indent: f32,
1717 pub text_indent_each_line: bool,
1718 pub text_indent_hanging: bool,
1719 pub initial_letter: Option<InitialLetter>,
1720 pub line_clamp: Option<NonZeroUsize>,
1721
1722 pub text_wrap: TextWrap,
1724 pub columns: u32,
1725 pub column_gap: f32,
1726 pub hanging_punctuation: bool,
1727 pub overflow_wrap: OverflowWrap,
1728 pub text_align_last: TextAlign,
1729 pub word_break: WordBreak,
1731 pub white_space_mode: WhiteSpaceMode,
1732 pub line_break: LineBreakStrictness,
1733 pub unicode_bidi: UnicodeBidi,
1735}
1736
1737impl Default for UnifiedConstraints {
1738 fn default() -> Self {
1739 Self {
1740 shape_boundaries: Vec::new(),
1741 shape_exclusions: Vec::new(),
1742
1743 available_width: AvailableSpace::MaxContent,
1750 available_height: None,
1751 writing_mode: None,
1752 direction: None, text_orientation: TextOrientation::default(),
1754 text_align: TextAlign::default(),
1755 text_justify: JustifyContent::default(),
1756 line_height: LineHeight::Normal,
1757 vertical_align: VerticalAlign::default(),
1758 strut_ascent: DEFAULT_STRUT_ASCENT,
1759 strut_descent: DEFAULT_STRUT_DESCENT,
1760 strut_x_height: DEFAULT_X_HEIGHT,
1761 ch_width: DEFAULT_CH_WIDTH,
1762 overflow: OverflowBehavior::default(),
1763 segment_alignment: SegmentAlignment::default(),
1764 text_combine_upright: None,
1765 exclusion_margin: 0.0,
1766 hyphenation: Hyphens::default(),
1767 hyphenation_language: None,
1768 columns: 1,
1769 column_gap: 0.0,
1770 hanging_punctuation: false,
1771 text_indent: 0.0,
1772 text_indent_each_line: false,
1773 text_indent_hanging: false,
1774 initial_letter: None,
1775 line_clamp: None,
1776 text_wrap: TextWrap::default(),
1777 overflow_wrap: OverflowWrap::default(),
1778 text_align_last: TextAlign::default(),
1779 word_break: WordBreak::default(),
1780 white_space_mode: WhiteSpaceMode::default(),
1781 line_break: LineBreakStrictness::default(),
1782 unicode_bidi: UnicodeBidi::default(),
1783 }
1784 }
1785}
1786
1787impl Hash for UnifiedConstraints {
1789 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
1791 self.shape_boundaries.hash(state);
1792 self.shape_exclusions.hash(state);
1793 self.available_width.hash(state);
1794 self.available_height
1795 .map(|h| h.round() as isize)
1796 .hash(state);
1797 self.writing_mode.hash(state);
1798 self.direction.hash(state);
1799 self.text_orientation.hash(state);
1800 self.text_align.hash(state);
1801 self.text_justify.hash(state);
1802 self.line_height.hash(state);
1803 self.vertical_align.hash(state);
1804 (self.strut_ascent.round() as isize).hash(state);
1805 (self.strut_descent.round() as isize).hash(state);
1806 (self.strut_x_height.round() as isize).hash(state);
1807 (self.ch_width.round() as isize).hash(state);
1808 self.overflow.hash(state);
1809 self.segment_alignment.hash(state);
1810 self.text_combine_upright.hash(state);
1811 (self.exclusion_margin.round() as isize).hash(state);
1812 self.hyphenation.hash(state);
1813 self.hyphenation_language.hash(state);
1814 (self.text_indent.round() as isize).hash(state);
1815 self.text_indent_each_line.hash(state);
1816 self.text_indent_hanging.hash(state);
1817 self.initial_letter.hash(state);
1818 self.line_clamp.hash(state);
1819 self.columns.hash(state);
1820 (self.column_gap.round() as isize).hash(state);
1821 self.hanging_punctuation.hash(state);
1822 self.overflow_wrap.hash(state);
1823 self.text_align_last.hash(state);
1824 self.word_break.hash(state);
1825 self.white_space_mode.hash(state);
1826 self.line_break.hash(state);
1827 self.unicode_bidi.hash(state);
1828 }
1829}
1830
1831impl PartialEq for UnifiedConstraints {
1832 fn eq(&self, other: &Self) -> bool {
1833 self.shape_boundaries == other.shape_boundaries
1834 && self.shape_exclusions == other.shape_exclusions
1835 && self.available_width == other.available_width
1836 && match (self.available_height, other.available_height) {
1837 (None, None) => true,
1838 (Some(h1), Some(h2)) => round_eq(h1, h2),
1839 _ => false,
1840 }
1841 && self.writing_mode == other.writing_mode
1842 && self.direction == other.direction
1843 && self.text_orientation == other.text_orientation
1844 && self.text_align == other.text_align
1845 && self.text_justify == other.text_justify
1846 && self.line_height == other.line_height
1847 && self.vertical_align == other.vertical_align
1848 && round_eq(self.strut_ascent, other.strut_ascent)
1849 && round_eq(self.strut_descent, other.strut_descent)
1850 && round_eq(self.strut_x_height, other.strut_x_height)
1851 && round_eq(self.ch_width, other.ch_width)
1852 && self.overflow == other.overflow
1853 && self.segment_alignment == other.segment_alignment
1854 && self.text_combine_upright == other.text_combine_upright
1855 && round_eq(self.exclusion_margin, other.exclusion_margin)
1856 && self.hyphenation == other.hyphenation
1857 && self.hyphenation_language == other.hyphenation_language
1858 && round_eq(self.text_indent, other.text_indent)
1859 && self.text_indent_each_line == other.text_indent_each_line
1860 && self.text_indent_hanging == other.text_indent_hanging
1861 && self.initial_letter == other.initial_letter
1862 && self.line_clamp == other.line_clamp
1863 && self.columns == other.columns
1864 && round_eq(self.column_gap, other.column_gap)
1865 && self.hanging_punctuation == other.hanging_punctuation
1866 && self.overflow_wrap == other.overflow_wrap
1867 && self.text_align_last == other.text_align_last
1868 && self.word_break == other.word_break
1869 && self.white_space_mode == other.white_space_mode
1870 && self.line_break == other.line_break
1871 && self.unicode_bidi == other.unicode_bidi
1872 }
1873}
1874
1875impl Eq for UnifiedConstraints {}
1876
1877impl UnifiedConstraints {
1878 #[must_use] pub fn resolved_line_height(&self) -> f32 {
1881 match self.line_height {
1882 LineHeight::Normal => self.strut_ascent + self.strut_descent,
1890 LineHeight::Px(px) => px,
1891 }
1892 }
1893 fn direction(&self, fallback: BidiDirection) -> BidiDirection {
1894 self.writing_mode.map_or(fallback, |s| s.get_direction().unwrap_or(fallback))
1895 }
1896 const fn is_vertical(&self) -> bool {
1897 matches!(
1898 self.writing_mode,
1899 Some(WritingMode::VerticalRl | WritingMode::VerticalLr)
1900 )
1901 }
1902}
1903
1904#[derive(Debug, Clone)]
1906pub struct LineConstraints {
1907 pub segments: Vec<LineSegment>,
1908 pub total_available: f32,
1909 pub is_min_content: bool,
1913}
1914
1915impl WritingMode {
1916 #[allow(clippy::trivially_copy_pass_by_ref)] #[allow(clippy::match_same_arms)] const fn get_direction(&self) -> Option<BidiDirection> {
1919 match self {
1920 Self::HorizontalTb => None,
1922 Self::VerticalRl => Some(BidiDirection::Rtl),
1923 Self::VerticalLr => Some(BidiDirection::Ltr),
1924 Self::SidewaysRl => Some(BidiDirection::Rtl),
1925 Self::SidewaysLr => Some(BidiDirection::Ltr),
1926 }
1927 }
1928}
1929
1930#[derive(Debug, Clone, Hash)]
1932pub struct StyledRun {
1933 pub text: String,
1934 pub style: Arc<StyleProperties>,
1935 pub logical_start_byte: usize,
1937 pub source_node_id: Option<NodeId>,
1940}
1941
1942#[derive(Debug, Clone)]
1944pub struct VisualRun<'a> {
1945 pub text_slice: &'a str,
1946 pub style: Arc<StyleProperties>,
1947 pub logical_start_byte: usize,
1948 pub bidi_level: BidiLevel,
1949 pub script: Script,
1950 pub language: Language,
1951}
1952
1953#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1958pub struct FontSelector {
1959 pub family: String,
1960 pub weight: FcWeight,
1961 pub style: FontStyle,
1962 pub unicode_ranges: Vec<UnicodeRange>,
1963}
1964
1965impl Default for FontSelector {
1966 fn default() -> Self {
1967 Self {
1968 family: "serif".to_string(),
1969 weight: FcWeight::Normal,
1970 style: FontStyle::Normal,
1971 unicode_ranges: Vec::new(),
1972 }
1973 }
1974}
1975
1976#[derive(Debug, Clone)]
1986#[repr(C, u8)]
1987pub enum FontStack {
1988 Stack(Vec<FontSelector>),
1991 Ref(azul_css::props::basic::font::FontRef),
1994}
1995
1996impl Default for FontStack {
1997 fn default() -> Self {
1998 Self::Stack(vec![FontSelector::default()])
1999 }
2000}
2001
2002impl FontStack {
2003 #[must_use] pub const fn is_ref(&self) -> bool {
2005 matches!(self, Self::Ref(_))
2006 }
2007
2008 #[must_use] pub const fn as_ref(&self) -> Option<&azul_css::props::basic::font::FontRef> {
2010 match self {
2011 Self::Ref(r) => Some(r),
2012 Self::Stack(_) => None,
2013 }
2014 }
2015
2016 #[must_use] pub fn as_stack(&self) -> Option<&[FontSelector]> {
2018 match self {
2019 Self::Stack(s) => Some(s),
2020 Self::Ref(_) => None,
2021 }
2022 }
2023
2024 #[must_use] pub fn first_selector(&self) -> Option<&FontSelector> {
2026 match self {
2027 Self::Stack(s) => s.first(),
2028 Self::Ref(_) => None,
2029 }
2030 }
2031
2032 #[must_use] pub fn first_family(&self) -> &str {
2034 match self {
2035 Self::Stack(s) => s.first().map_or("serif", |f| f.family.as_str()),
2036 Self::Ref(_) => "<embedded-font>",
2037 }
2038 }
2039}
2040
2041impl PartialEq for FontStack {
2042 fn eq(&self, other: &Self) -> bool {
2043 match (self, other) {
2044 (Self::Stack(a), Self::Stack(b)) => a == b,
2045 (Self::Ref(a), Self::Ref(b)) => a.parsed == b.parsed,
2046 _ => false,
2047 }
2048 }
2049}
2050
2051impl Eq for FontStack {}
2052
2053impl Hash for FontStack {
2054 fn hash<H: Hasher>(&self, state: &mut H) {
2055 discriminant(self).hash(state);
2056 match self {
2057 Self::Stack(s) => s.hash(state),
2058 Self::Ref(r) => (r.parsed as usize).hash(state),
2059 }
2060 }
2061}
2062
2063#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2067pub struct FontHash {
2068 pub font_hash: u64,
2070}
2071
2072impl FontHash {
2073 #[must_use] pub const fn invalid() -> Self {
2074 Self { font_hash: 0 }
2075 }
2076
2077 #[must_use] pub const fn from_hash(font_hash: u64) -> Self {
2078 Self { font_hash }
2079 }
2080}
2081
2082#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2083pub enum FontStyle {
2084 Normal,
2085 Italic,
2086 Oblique,
2087}
2088
2089#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
2091pub enum SegmentAlignment {
2092 #[default]
2094 First,
2095 Total,
2098}
2099
2100#[derive(Copy, Debug, Clone)]
2101pub struct VerticalMetrics {
2102 pub advance: f32,
2103 pub bearing_x: f32,
2104 pub bearing_y: f32,
2105 pub origin_y: f32,
2106}
2107
2108#[derive(Copy, Debug, Clone)]
2120pub struct LayoutFontMetrics {
2121 pub ascent: f32,
2122 pub descent: f32,
2123 pub line_gap: f32,
2124 pub units_per_em: u16,
2125 pub x_height: Option<f32>,
2128 pub cap_height: Option<f32>,
2131}
2132
2133impl LayoutFontMetrics {
2134 #[must_use] pub fn baseline_scaled(&self, font_size: f32) -> f32 {
2138 let scale = font_size / f32::from(self.units_per_em);
2139 self.ascent * scale
2140 }
2141
2142 #[must_use] pub fn x_height_scaled(&self, font_size: f32) -> f32 {
2145 let scale = font_size / f32::from(self.units_per_em);
2146 self.x_height.map_or(font_size * 0.5, |xh| xh * scale)
2147 }
2148
2149 #[must_use] pub fn cap_height_scaled(&self, font_size: f32) -> f32 {
2152 let scale = font_size / f32::from(self.units_per_em);
2153 self.cap_height.unwrap_or(self.ascent) * scale
2154 }
2155
2156 #[must_use] pub fn from_font_metrics(metrics: &azul_css::props::basic::FontMetrics) -> Self {
2176 let ascent = metrics.s_typo_ascender
2177 .as_option()
2178 .map_or_else(|| f32::from(metrics.ascender), |v| f32::from(*v));
2179 let descent = metrics.s_typo_descender
2180 .as_option()
2181 .map_or_else(|| f32::from(metrics.descender), |v| f32::from(*v));
2182 let line_gap = metrics.s_typo_line_gap
2185 .as_option()
2186 .map_or_else(|| f32::from(metrics.line_gap), |v| f32::from(*v))
2187 .max(0.0);
2188 let x_height = metrics.sx_height
2189 .as_option()
2190 .map(|v| f32::from(*v));
2191 let cap_height = metrics.s_cap_height
2192 .as_option()
2193 .map(|v| f32::from(*v));
2194 Self {
2195 ascent,
2196 descent,
2197 line_gap,
2198 units_per_em: metrics.units_per_em,
2199 x_height,
2200 cap_height,
2201 }
2202 }
2203
2204 #[must_use] pub fn em_over(&self) -> f32 {
2209 let central = self.central_baseline();
2210 central + (f32::from(self.units_per_em) / 2.0)
2211 }
2212
2213 #[must_use] pub fn em_under(&self) -> f32 {
2216 let central = self.central_baseline();
2217 central - (f32::from(self.units_per_em) / 2.0)
2218 }
2219
2220 #[must_use] pub const fn central_baseline(&self) -> f32 {
2223 f32::midpoint(self.ascent, self.descent)
2224 }
2225}
2226
2227#[derive(Copy, Debug, Clone)]
2228pub struct LineSegment {
2229 pub start_x: f32,
2230 pub width: f32,
2231 pub priority: u8,
2233}
2234
2235#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2236pub enum TextWrap {
2237 #[default]
2238 Wrap,
2239 Balance,
2240 NoWrap,
2241}
2242
2243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
2248pub enum OverflowWrap {
2249 #[default]
2251 Normal,
2252 Anywhere,
2256 BreakWord,
2260}
2261
2262#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
2266pub enum Hyphens {
2267 None,
2269 #[default]
2271 Manual,
2272 Auto,
2274}
2275
2276#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2280pub enum WhiteSpaceMode {
2281 #[default]
2282 Normal,
2283 Nowrap,
2284 Pre,
2285 PreWrap,
2286 PreLine,
2287 BreakSpaces,
2288}
2289
2290#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2297pub enum LineBreakStrictness {
2298 #[default]
2299 Auto,
2300 Loose,
2301 Normal,
2302 Strict,
2303 Anywhere,
2306}
2307
2308#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
2310pub enum WordBreak {
2311 #[default]
2314 Normal,
2315 BreakAll,
2317 KeepAll,
2320}
2321
2322#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
2330pub struct InitialLetter {
2331 pub size: f32,
2333 pub sink: u32,
2336 pub count: NonZeroUsize,
2338 pub align: InitialLetterAlign,
2342}
2343
2344#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2347pub enum InitialLetterAlign {
2348 Auto,
2350 Alphabetic,
2352 Hanging,
2354 Ideographic,
2356}
2357
2358impl Eq for InitialLetter {}
2363
2364impl Hash for InitialLetter {
2365 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
2367 (self.size.round() as isize).hash(state);
2372 self.sink.hash(state);
2373 self.count.hash(state);
2374 self.align.hash(state);
2375 }
2376}
2377
2378#[derive(Copy, Debug, Clone, PartialOrd)]
2380pub enum PathSegment {
2381 MoveTo(Point),
2382 LineTo(Point),
2383 CurveTo {
2384 control1: Point,
2385 control2: Point,
2386 end: Point,
2387 },
2388 QuadTo {
2389 control: Point,
2390 end: Point,
2391 },
2392 Arc {
2393 center: Point,
2394 radius: f32,
2395 start_angle: f32,
2396 end_angle: f32,
2397 },
2398 Close,
2399}
2400
2401impl Hash for PathSegment {
2403 #[allow(clippy::cast_possible_truncation)] #[allow(clippy::match_same_arms)] fn hash<H: Hasher>(&self, state: &mut H) {
2406 discriminant(self).hash(state);
2408
2409 match self {
2410 Self::MoveTo(p) => p.hash(state),
2411 Self::LineTo(p) => p.hash(state),
2412 Self::CurveTo {
2413 control1,
2414 control2,
2415 end,
2416 } => {
2417 control1.hash(state);
2418 control2.hash(state);
2419 end.hash(state);
2420 }
2421 Self::QuadTo { control, end } => {
2422 control.hash(state);
2423 end.hash(state);
2424 }
2425 Self::Arc {
2426 center,
2427 radius,
2428 start_angle,
2429 end_angle,
2430 } => {
2431 center.hash(state);
2432 (radius.round() as isize).hash(state);
2433 (start_angle.round() as isize).hash(state);
2434 (end_angle.round() as isize).hash(state);
2435 }
2436 Self::Close => {} }
2438 }
2439}
2440
2441impl PartialEq for PathSegment {
2442 #[allow(clippy::similar_names)] #[allow(clippy::match_same_arms)] fn eq(&self, other: &Self) -> bool {
2445 match (self, other) {
2446 (Self::MoveTo(a), Self::MoveTo(b)) => a == b,
2447 (Self::LineTo(a), Self::LineTo(b)) => a == b,
2448 (
2449 Self::CurveTo {
2450 control1: c1a,
2451 control2: c2a,
2452 end: ea,
2453 },
2454 Self::CurveTo {
2455 control1: c1b,
2456 control2: c2b,
2457 end: eb,
2458 },
2459 ) => c1a == c1b && c2a == c2b && ea == eb,
2460 (
2461 Self::QuadTo {
2462 control: ca,
2463 end: ea,
2464 },
2465 Self::QuadTo {
2466 control: cb,
2467 end: eb,
2468 },
2469 ) => ca == cb && ea == eb,
2470 (
2471 Self::Arc {
2472 center: ca,
2473 radius: ra,
2474 start_angle: sa_a,
2475 end_angle: ea_a,
2476 },
2477 Self::Arc {
2478 center: cb,
2479 radius: rb,
2480 start_angle: sa_b,
2481 end_angle: ea_b,
2482 },
2483 ) => ca == cb && round_eq(*ra, *rb) && round_eq(*sa_a, *sa_b) && round_eq(*ea_a, *ea_b),
2484 (Self::Close, Self::Close) => true,
2485 _ => false, }
2487 }
2488}
2489
2490impl Eq for PathSegment {}
2491
2492#[derive(Debug, Clone, Hash)]
2501#[repr(C, u8)]
2502pub enum InlineContent {
2503 Text(StyledRun),
2504 Image(InlineImage),
2505 Shape(InlineShape),
2506 Space(InlineSpace),
2507 LineBreak(InlineBreak),
2508 Tab {
2510 style: Arc<StyleProperties>,
2511 },
2512 Marker {
2516 run: StyledRun,
2517 position_outside: bool,
2519 },
2520 Ruby {
2522 base: Vec<InlineContent>,
2523 text: Vec<InlineContent>,
2524 style: Arc<StyleProperties>,
2526 },
2527}
2528
2529#[derive(Debug, Clone)]
2530pub struct InlineImage {
2531 pub source: ImageSource,
2532 pub intrinsic_size: Size,
2533 pub display_size: Option<Size>,
2534 pub baseline_offset: f32,
2536 pub alignment: VerticalAlign,
2537 pub object_fit: ObjectFit,
2538}
2539
2540impl PartialEq for InlineImage {
2541 fn eq(&self, other: &Self) -> bool {
2542 self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
2543 && self.source == other.source
2544 && self.intrinsic_size == other.intrinsic_size
2545 && self.display_size == other.display_size
2546 && self.alignment == other.alignment
2547 && self.object_fit == other.object_fit
2548 }
2549}
2550
2551impl Eq for InlineImage {}
2552
2553impl Hash for InlineImage {
2554 fn hash<H: Hasher>(&self, state: &mut H) {
2555 self.source.hash(state);
2556 self.intrinsic_size.hash(state);
2557 self.display_size.hash(state);
2558 self.baseline_offset.to_bits().hash(state);
2559 self.alignment.hash(state);
2560 self.object_fit.hash(state);
2561 }
2562}
2563
2564impl PartialOrd for InlineImage {
2565 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2566 Some(self.cmp(other))
2567 }
2568}
2569
2570impl Ord for InlineImage {
2571 fn cmp(&self, other: &Self) -> Ordering {
2572 self.source
2573 .cmp(&other.source)
2574 .then_with(|| self.intrinsic_size.cmp(&other.intrinsic_size))
2575 .then_with(|| self.display_size.cmp(&other.display_size))
2576 .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
2577 .then_with(|| self.alignment.cmp(&other.alignment))
2578 .then_with(|| self.object_fit.cmp(&other.object_fit))
2579 }
2580}
2581
2582#[derive(Debug, Clone)]
2584pub struct Glyph {
2585 pub glyph_id: u16,
2587 pub codepoint: char,
2588 pub font_hash: u64,
2590 pub font_metrics: LayoutFontMetrics,
2592 pub style: Arc<StyleProperties>,
2593 pub source: GlyphSource,
2594
2595 pub logical_byte_index: usize,
2597 pub logical_byte_len: usize,
2598 pub content_index: usize,
2599 pub cluster: u32,
2600
2601 pub advance: f32,
2603 pub kerning: f32,
2604 pub offset: Point,
2605
2606 pub vertical_advance: f32,
2608 pub vertical_origin_y: f32, pub vertical_bearing: Point,
2610 pub orientation: GlyphOrientation,
2611
2612 pub script: Script,
2614 pub bidi_level: BidiLevel,
2615}
2616
2617impl Glyph {
2618 #[inline]
2619 fn bounds(&self) -> Rect {
2620 Rect {
2621 x: 0.0,
2622 y: 0.0,
2623 width: self.advance,
2624 height: self.style.line_height.resolve_with_metrics(self.style.font_size_px, &self.font_metrics),
2625 }
2626 }
2627
2628 #[inline]
2629 const fn character_class(&self) -> CharacterClass {
2630 classify_character(self.codepoint as u32)
2631 }
2632
2633 #[inline]
2634 fn is_whitespace(&self) -> bool {
2635 self.character_class() == CharacterClass::Space
2636 }
2637
2638 #[inline]
2639 fn can_justify(&self) -> bool {
2640 !self.codepoint.is_whitespace() && self.character_class() != CharacterClass::Combining
2641 }
2642
2643 #[inline]
2644 const fn justification_priority(&self) -> u8 {
2645 get_justification_priority(self.character_class())
2646 }
2647
2648 #[inline]
2649 const fn break_opportunity_after(&self) -> bool {
2650 let is_whitespace = self.codepoint.is_whitespace();
2651 let is_soft_hyphen = self.codepoint == '\u{00AD}';
2652 let is_hyphen_minus = self.codepoint == '\u{002D}';
2653 let is_hyphen = self.codepoint == '\u{2010}';
2654 is_whitespace || is_soft_hyphen || is_hyphen_minus || is_hyphen
2655 }
2656}
2657
2658#[derive(Debug, Clone)]
2660pub(crate) struct TextRunInfo<'a> {
2661 pub(crate) text: &'a str,
2662 pub(crate) style: Arc<StyleProperties>,
2663 pub(crate) logical_start: usize,
2664 pub(crate) content_index: usize,
2665}
2666
2667#[derive(Debug, Clone)]
2668pub enum ImageSource {
2669 Ref(ImageRef),
2671 Url(String),
2673 Data(Arc<[u8]>),
2675 Svg(Arc<str>),
2677 Placeholder(Size),
2679}
2680
2681impl PartialEq for ImageSource {
2682 fn eq(&self, other: &Self) -> bool {
2683 match (self, other) {
2684 (Self::Ref(a), Self::Ref(b)) => a.get_hash() == b.get_hash(),
2685 (Self::Url(a), Self::Url(b)) => a == b,
2686 (Self::Data(a), Self::Data(b)) => Arc::ptr_eq(a, b),
2687 (Self::Svg(a), Self::Svg(b)) => Arc::ptr_eq(a, b),
2688 (Self::Placeholder(a), Self::Placeholder(b)) => {
2689 a.width.to_bits() == b.width.to_bits() && a.height.to_bits() == b.height.to_bits()
2690 }
2691 _ => false,
2692 }
2693 }
2694}
2695
2696impl Eq for ImageSource {}
2697
2698impl Hash for ImageSource {
2699 fn hash<H: Hasher>(&self, state: &mut H) {
2700 discriminant(self).hash(state);
2701 match self {
2702 Self::Ref(r) => r.get_hash().hash(state),
2703 Self::Url(s) => s.hash(state),
2704 Self::Data(d) => (Arc::as_ptr(d).cast::<u8>() as usize).hash(state),
2705 Self::Svg(s) => (Arc::as_ptr(s).cast::<u8>() as usize).hash(state),
2706 Self::Placeholder(sz) => {
2707 sz.width.to_bits().hash(state);
2708 sz.height.to_bits().hash(state);
2709 }
2710 }
2711 }
2712}
2713
2714impl PartialOrd for ImageSource {
2715 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2716 Some(self.cmp(other))
2717 }
2718}
2719
2720impl Ord for ImageSource {
2721 fn cmp(&self, other: &Self) -> Ordering {
2722 const fn variant_index(s: &ImageSource) -> u8 {
2723 match s {
2724 ImageSource::Ref(_) => 0,
2725 ImageSource::Url(_) => 1,
2726 ImageSource::Data(_) => 2,
2727 ImageSource::Svg(_) => 3,
2728 ImageSource::Placeholder(_) => 4,
2729 }
2730 }
2731 match (self, other) {
2732 (Self::Ref(a), Self::Ref(b)) => a.get_hash().cmp(&b.get_hash()),
2733 (Self::Url(a), Self::Url(b)) => a.cmp(b),
2734 (Self::Data(a), Self::Data(b)) => {
2735 (Arc::as_ptr(a).cast::<u8>() as usize).cmp(&(Arc::as_ptr(b).cast::<u8>() as usize))
2736 }
2737 (Self::Svg(a), Self::Svg(b)) => {
2738 (Arc::as_ptr(a).cast::<u8>() as usize).cmp(&(Arc::as_ptr(b).cast::<u8>() as usize))
2739 }
2740 (Self::Placeholder(a), Self::Placeholder(b)) => {
2741 (a.width.to_bits(), a.height.to_bits())
2742 .cmp(&(b.width.to_bits(), b.height.to_bits()))
2743 }
2744 _ => variant_index(self).cmp(&variant_index(other)),
2746 }
2747 }
2748}
2749
2750#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
2756pub enum VerticalAlign {
2757 #[default]
2759 Baseline,
2760 Bottom,
2762 Top,
2764 Middle,
2766 TextTop,
2768 TextBottom,
2770 Sub,
2772 Super,
2774 Offset(f32),
2776}
2777
2778impl Hash for VerticalAlign {
2779 fn hash<H: Hasher>(&self, state: &mut H) {
2780 discriminant(self).hash(state);
2781 if let Self::Offset(f) = self {
2782 f.to_bits().hash(state);
2783 }
2784 }
2785}
2786
2787impl Eq for VerticalAlign {}
2788
2789#[allow(clippy::derive_ord_xor_partial_ord)]
2792impl Ord for VerticalAlign {
2793 fn cmp(&self, other: &Self) -> Ordering {
2794 self.partial_cmp(other).unwrap_or(Ordering::Equal)
2795 }
2796}
2797
2798#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
2799pub enum ObjectFit {
2800 Fill,
2802 Contain,
2804 Cover,
2806 None,
2808 ScaleDown,
2810}
2811
2812#[derive(Copy, Debug, Clone, PartialEq)]
2818pub struct InlineBorderInfo {
2819 pub top: f32,
2821 pub right: f32,
2822 pub bottom: f32,
2823 pub left: f32,
2824 pub top_color: ColorU,
2826 pub right_color: ColorU,
2827 pub bottom_color: ColorU,
2828 pub left_color: ColorU,
2829 pub radius: Option<f32>,
2831 pub padding_top: f32,
2833 pub padding_right: f32,
2834 pub padding_bottom: f32,
2835 pub padding_left: f32,
2836 pub is_first_fragment: bool,
2841 pub is_last_fragment: bool,
2843 pub is_rtl: bool,
2847}
2848
2849impl Default for InlineBorderInfo {
2850 fn default() -> Self {
2851 Self {
2852 top: 0.0,
2853 right: 0.0,
2854 bottom: 0.0,
2855 left: 0.0,
2856 top_color: ColorU::TRANSPARENT,
2857 right_color: ColorU::TRANSPARENT,
2858 bottom_color: ColorU::TRANSPARENT,
2859 left_color: ColorU::TRANSPARENT,
2860 radius: None,
2861 padding_top: 0.0,
2862 padding_right: 0.0,
2863 padding_bottom: 0.0,
2864 padding_left: 0.0,
2865 is_first_fragment: true,
2866 is_last_fragment: true,
2867 is_rtl: false,
2868 }
2869 }
2870}
2871
2872impl InlineBorderInfo {
2873 #[must_use] pub fn has_border(&self) -> bool {
2875 self.top > 0.0 || self.right > 0.0 || self.bottom > 0.0 || self.left > 0.0
2876 }
2877
2878 #[must_use] pub fn has_chrome(&self) -> bool {
2880 self.has_border()
2881 || self.padding_top > 0.0
2882 || self.padding_right > 0.0
2883 || self.padding_bottom > 0.0
2884 || self.padding_left > 0.0
2885 }
2886
2887 #[must_use] pub fn left_inset(&self) -> f32 {
2896 let show = if self.is_rtl { self.is_last_fragment } else { self.is_first_fragment };
2897 if show { self.left + self.padding_left } else { 0.0 }
2898 }
2899 #[must_use] pub fn right_inset(&self) -> f32 {
2902 let show = if self.is_rtl { self.is_first_fragment } else { self.is_last_fragment };
2903 if show { self.right + self.padding_right } else { 0.0 }
2904 }
2905 #[must_use] pub fn top_inset(&self) -> f32 { self.top + self.padding_top }
2907 #[must_use] pub fn bottom_inset(&self) -> f32 { self.bottom + self.padding_bottom }
2909}
2910
2911#[derive(Debug, Clone)]
2912pub struct InlineShape {
2913 pub shape_def: ShapeDefinition,
2914 pub fill: Option<ColorU>,
2915 pub stroke: Option<Stroke>,
2916 pub baseline_offset: f32,
2917 pub alignment: VerticalAlign,
2920 pub source_node_id: Option<NodeId>,
2924}
2925
2926#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
2927pub enum OverflowBehavior {
2928 Visible,
2930 Hidden,
2932 Scroll,
2934 #[default]
2936 Auto,
2937 Break,
2939}
2940
2941#[derive(Debug, Clone)]
2942pub(crate) struct MeasuredImage {
2943 pub(crate) source: ImageSource,
2944 pub(crate) size: Size,
2945 pub(crate) baseline_offset: f32,
2946 pub(crate) alignment: VerticalAlign,
2947 pub(crate) content_index: usize,
2948}
2949
2950#[derive(Debug, Clone)]
2951pub(crate) struct MeasuredShape {
2952 pub(crate) shape_def: ShapeDefinition,
2953 pub(crate) size: Size,
2954 pub(crate) baseline_offset: f32,
2955 pub(crate) alignment: VerticalAlign,
2956 pub(crate) content_index: usize,
2957}
2958
2959#[derive(Copy, Debug, Clone)]
2960pub struct InlineSpace {
2961 pub width: f32,
2962 pub is_breaking: bool, pub is_stretchy: bool, }
2965
2966impl PartialEq for InlineSpace {
2967 fn eq(&self, other: &Self) -> bool {
2968 self.width.to_bits() == other.width.to_bits()
2969 && self.is_breaking == other.is_breaking
2970 && self.is_stretchy == other.is_stretchy
2971 }
2972}
2973
2974impl Eq for InlineSpace {}
2975
2976impl Hash for InlineSpace {
2977 fn hash<H: Hasher>(&self, state: &mut H) {
2978 self.width.to_bits().hash(state);
2979 self.is_breaking.hash(state);
2980 self.is_stretchy.hash(state);
2981 }
2982}
2983
2984impl PartialOrd for InlineSpace {
2985 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2986 Some(self.cmp(other))
2987 }
2988}
2989
2990impl Ord for InlineSpace {
2991 fn cmp(&self, other: &Self) -> Ordering {
2992 self.width
2993 .total_cmp(&other.width)
2994 .then_with(|| self.is_breaking.cmp(&other.is_breaking))
2995 .then_with(|| self.is_stretchy.cmp(&other.is_stretchy))
2996 }
2997}
2998
2999impl PartialEq for InlineShape {
3000 fn eq(&self, other: &Self) -> bool {
3001 self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
3002 && self.shape_def == other.shape_def
3003 && self.fill == other.fill
3004 && self.stroke == other.stroke
3005 && self.alignment == other.alignment
3006 && self.source_node_id == other.source_node_id
3007 }
3008}
3009
3010impl Eq for InlineShape {}
3011
3012impl Hash for InlineShape {
3013 fn hash<H: Hasher>(&self, state: &mut H) {
3014 self.shape_def.hash(state);
3015 self.fill.hash(state);
3016 self.stroke.hash(state);
3017 self.baseline_offset.to_bits().hash(state);
3018 self.alignment.hash(state);
3019 self.source_node_id.hash(state);
3020 }
3021}
3022
3023impl PartialOrd for InlineShape {
3024 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
3025 Some(
3026 self.shape_def
3027 .partial_cmp(&other.shape_def)?
3028 .then_with(|| self.fill.cmp(&other.fill))
3029 .then_with(|| {
3030 self.stroke
3031 .partial_cmp(&other.stroke)
3032 .unwrap_or(Ordering::Equal)
3033 })
3034 .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
3035 .then_with(|| self.alignment.cmp(&other.alignment))
3036 .then_with(|| self.source_node_id.cmp(&other.source_node_id)),
3037 )
3038 }
3039}
3040
3041#[derive(Debug, Default, Clone, Copy)]
3042pub struct Rect {
3043 pub x: f32,
3044 pub y: f32,
3045 pub width: f32,
3046 pub height: f32,
3047}
3048
3049impl PartialEq for Rect {
3050 fn eq(&self, other: &Self) -> bool {
3051 round_eq(self.x, other.x)
3052 && round_eq(self.y, other.y)
3053 && round_eq(self.width, other.width)
3054 && round_eq(self.height, other.height)
3055 }
3056}
3057impl Eq for Rect {}
3058
3059impl Hash for Rect {
3060 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3062 (self.x.round() as isize).hash(state);
3065 (self.y.round() as isize).hash(state);
3066 (self.width.round() as isize).hash(state);
3067 (self.height.round() as isize).hash(state);
3068 }
3069}
3070
3071#[derive(Debug, Default, Clone, Copy)]
3072pub struct Size {
3073 pub width: f32,
3074 pub height: f32,
3075}
3076
3077impl PartialOrd for Size {
3078 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
3079 Some(self.cmp(other))
3080 }
3081}
3082
3083impl Ord for Size {
3084 #[allow(clippy::cast_possible_truncation)] fn cmp(&self, other: &Self) -> Ordering {
3086 (self.width.round() as isize)
3087 .cmp(&(other.width.round() as isize))
3088 .then_with(|| (self.height.round() as isize).cmp(&(other.height.round() as isize)))
3089 }
3090}
3091
3092impl Hash for Size {
3094 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3096 (self.width.round() as isize).hash(state);
3097 (self.height.round() as isize).hash(state);
3098 }
3099}
3100impl PartialEq for Size {
3101 fn eq(&self, other: &Self) -> bool {
3102 round_eq(self.width, other.width) && round_eq(self.height, other.height)
3103 }
3104}
3105impl Eq for Size {}
3106
3107impl Size {
3108 #[must_use] pub const fn zero() -> Self {
3109 Self::new(0.0, 0.0)
3110 }
3111 #[must_use] pub const fn new(width: f32, height: f32) -> Self {
3112 Self { width, height }
3113 }
3114}
3115
3116#[derive(Debug, Default, Clone, Copy, PartialOrd)]
3117pub struct Point {
3118 pub x: f32,
3119 pub y: f32,
3120}
3121
3122impl Hash for Point {
3124 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3126 (self.x.round() as isize).hash(state);
3127 (self.y.round() as isize).hash(state);
3128 }
3129}
3130
3131impl PartialEq for Point {
3132 fn eq(&self, other: &Self) -> bool {
3133 round_eq(self.x, other.x) && round_eq(self.y, other.y)
3134 }
3135}
3136
3137impl Eq for Point {}
3138
3139#[derive(Debug, Clone, PartialOrd)]
3140pub enum ShapeDefinition {
3141 Rectangle {
3142 size: Size,
3143 corner_radius: Option<f32>,
3144 },
3145 Circle {
3146 radius: f32,
3147 },
3148 Ellipse {
3149 radii: Size,
3150 },
3151 Polygon {
3152 points: Vec<Point>,
3153 },
3154 Path {
3155 segments: Vec<PathSegment>,
3156 },
3157}
3158
3159impl Hash for ShapeDefinition {
3161 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3163 discriminant(self).hash(state);
3164 match self {
3165 Self::Rectangle {
3166 size,
3167 corner_radius,
3168 } => {
3169 size.hash(state);
3170 corner_radius.map(|r| r.round() as isize).hash(state);
3171 }
3172 Self::Circle { radius } => {
3173 (radius.round() as isize).hash(state);
3174 }
3175 Self::Ellipse { radii } => {
3176 radii.hash(state);
3177 }
3178 Self::Polygon { points } => {
3179 points.hash(state);
3181 }
3182 Self::Path { segments } => {
3183 segments.hash(state);
3185 }
3186 }
3187 }
3188}
3189
3190impl PartialEq for ShapeDefinition {
3191 fn eq(&self, other: &Self) -> bool {
3192 match (self, other) {
3193 (
3194 Self::Rectangle {
3195 size: s1,
3196 corner_radius: r1,
3197 },
3198 Self::Rectangle {
3199 size: s2,
3200 corner_radius: r2,
3201 },
3202 ) => {
3203 s1 == s2
3204 && match (r1, r2) {
3205 (None, None) => true,
3206 (Some(v1), Some(v2)) => round_eq(*v1, *v2),
3207 _ => false,
3208 }
3209 }
3210 (Self::Circle { radius: r1 }, Self::Circle { radius: r2 }) => {
3211 round_eq(*r1, *r2)
3212 }
3213 (Self::Ellipse { radii: r1 }, Self::Ellipse { radii: r2 }) => {
3214 r1 == r2
3215 }
3216 (Self::Polygon { points: p1 }, Self::Polygon { points: p2 }) => {
3217 p1 == p2
3218 }
3219 (Self::Path { segments: s1 }, Self::Path { segments: s2 }) => {
3220 s1 == s2
3221 }
3222 _ => false,
3223 }
3224 }
3225}
3226impl Eq for ShapeDefinition {}
3227
3228impl ShapeDefinition {
3229 #[must_use] pub fn get_size(&self) -> Size {
3231 match self {
3232 Self::Rectangle { size, .. } => *size,
3234
3235 Self::Circle { radius } => {
3237 let diameter = radius * 2.0;
3238 Size::new(diameter, diameter)
3239 }
3240
3241 Self::Ellipse { radii } => Size::new(radii.width * 2.0, radii.height * 2.0),
3243
3244 Self::Polygon { points } => calculate_bounding_box_size(points),
3246
3247 Self::Path { segments } => {
3254 let mut points = Vec::new();
3255 let mut current_pos = Point { x: 0.0, y: 0.0 };
3256
3257 for segment in segments {
3258 match segment {
3259 PathSegment::MoveTo(p) | PathSegment::LineTo(p) => {
3260 points.push(*p);
3261 current_pos = *p;
3262 }
3263 PathSegment::QuadTo { control, end } => {
3264 points.push(current_pos);
3265 points.push(*control);
3266 points.push(*end);
3267 current_pos = *end;
3268 }
3269 PathSegment::CurveTo {
3270 control1,
3271 control2,
3272 end,
3273 } => {
3274 points.push(current_pos);
3275 points.push(*control1);
3276 points.push(*control2);
3277 points.push(*end);
3278 current_pos = *end;
3279 }
3280 PathSegment::Arc {
3281 center,
3282 radius,
3283 start_angle,
3284 end_angle,
3285 } => {
3286 let start_point = Point {
3288 x: center.x + radius * start_angle.cos(),
3289 y: center.y + radius * start_angle.sin(),
3290 };
3291 let end_point = Point {
3292 x: center.x + radius * end_angle.cos(),
3293 y: center.y + radius * end_angle.sin(),
3294 };
3295 points.push(start_point);
3296 points.push(end_point);
3297
3298 let mut normalized_end = *end_angle;
3302 #[allow(clippy::while_float)] while normalized_end < *start_angle {
3304 normalized_end += 2.0 * std::f32::consts::PI;
3305 }
3306
3307 let mut check_angle = (*start_angle / std::f32::consts::FRAC_PI_2)
3310 .ceil()
3311 * std::f32::consts::FRAC_PI_2;
3312
3313 #[allow(clippy::while_float)] while check_angle < normalized_end {
3318 points.push(Point {
3319 x: center.x + radius * check_angle.cos(),
3320 y: center.y + radius * check_angle.sin(),
3321 });
3322 check_angle += std::f32::consts::FRAC_PI_2;
3323 }
3324
3325 current_pos = end_point;
3328 }
3329 PathSegment::Close => {
3330 }
3332 }
3333 }
3334 calculate_bounding_box_size(&points)
3335 }
3336 }
3337 }
3338}
3339
3340pub(crate) fn resolve_effective_alignment(
3348 text_align: TextAlign,
3349 text_align_last: TextAlign,
3350 is_last_or_forced: bool,
3351) -> TextAlign {
3352 if is_last_or_forced {
3353 if text_align_last == TextAlign::default() {
3354 if text_align == TextAlign::Justify { TextAlign::Start } else { text_align }
3355 } else {
3356 text_align_last
3357 }
3358 } else {
3359 text_align
3360 }
3361}
3362
3363fn calculate_bounding_box_size(points: &[Point]) -> Size {
3365 if points.is_empty() {
3366 return Size::zero();
3367 }
3368
3369 let mut min_x = f32::MAX;
3370 let mut max_x = f32::MIN;
3371 let mut min_y = f32::MAX;
3372 let mut max_y = f32::MIN;
3373
3374 for point in points {
3375 min_x = min_x.min(point.x);
3376 max_x = max_x.max(point.x);
3377 min_y = min_y.min(point.y);
3378 max_y = max_y.max(point.y);
3379 }
3380
3381 if min_x > max_x || min_y > max_y {
3383 return Size::zero();
3384 }
3385
3386 Size::new(max_x - min_x, max_y - min_y)
3387}
3388
3389#[derive(Debug, Clone, PartialOrd)]
3390pub struct Stroke {
3391 pub color: ColorU,
3392 pub width: f32,
3393 pub dash_pattern: Option<Vec<f32>>,
3394}
3395
3396impl Hash for Stroke {
3398 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3400 self.color.hash(state);
3401 (self.width.round() as isize).hash(state);
3402
3403 match &self.dash_pattern {
3405 None => 0u8.hash(state), Some(pattern) => {
3407 1u8.hash(state); pattern.len().hash(state); for &val in pattern {
3410 (val.round() as isize).hash(state); }
3412 }
3413 }
3414 }
3415}
3416
3417impl PartialEq for Stroke {
3418 fn eq(&self, other: &Self) -> bool {
3419 if self.color != other.color || !round_eq(self.width, other.width) {
3420 return false;
3421 }
3422 match (&self.dash_pattern, &other.dash_pattern) {
3423 (None, None) => true,
3424 (Some(p1), Some(p2)) => {
3425 p1.len() == p2.len() && p1.iter().zip(p2.iter()).all(|(a, b)| round_eq(*a, *b))
3426 }
3427 _ => false,
3428 }
3429 }
3430}
3431
3432impl Eq for Stroke {}
3433
3434#[allow(clippy::cast_possible_truncation)] fn round_eq(a: f32, b: f32) -> bool {
3437 (a.round() as isize) == (b.round() as isize)
3438}
3439
3440#[derive(Debug, Clone)]
3441pub enum ShapeBoundary {
3442 Rectangle(Rect),
3443 Circle { center: Point, radius: f32 },
3444 Ellipse { center: Point, radii: Size },
3445 Polygon { points: Vec<Point> },
3446 Path { segments: Vec<PathSegment> },
3447}
3448
3449impl ShapeBoundary {
3450 #[allow(clippy::suboptimal_flops)] #[must_use] pub fn inflate(&self, margin: f32) -> Self {
3452 if margin == 0.0 {
3453 return self.clone();
3454 }
3455 match self {
3456 Self::Rectangle(rect) => Self::Rectangle(Rect {
3457 x: rect.x - margin,
3458 y: rect.y - margin,
3459 width: (rect.width + margin * 2.0).max(0.0),
3460 height: (rect.height + margin * 2.0).max(0.0),
3461 }),
3462 Self::Circle { center, radius } => Self::Circle {
3463 center: *center,
3464 radius: radius + margin,
3465 },
3466 _ => self.clone(),
3469 }
3470 }
3471}
3472
3473impl Hash for ShapeBoundary {
3475 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3477 discriminant(self).hash(state);
3478 match self {
3479 Self::Rectangle(rect) => rect.hash(state),
3480 Self::Circle { center, radius } => {
3481 center.hash(state);
3482 (radius.round() as isize).hash(state);
3483 }
3484 Self::Ellipse { center, radii } => {
3485 center.hash(state);
3486 radii.hash(state);
3487 }
3488 Self::Polygon { points } => points.hash(state),
3489 Self::Path { segments } => segments.hash(state),
3490 }
3491 }
3492}
3493impl PartialEq for ShapeBoundary {
3494 fn eq(&self, other: &Self) -> bool {
3495 match (self, other) {
3496 (Self::Rectangle(r1), Self::Rectangle(r2)) => r1 == r2,
3497 (
3498 Self::Circle {
3499 center: c1,
3500 radius: r1,
3501 },
3502 Self::Circle {
3503 center: c2,
3504 radius: r2,
3505 },
3506 ) => c1 == c2 && round_eq(*r1, *r2),
3507 (
3508 Self::Ellipse {
3509 center: c1,
3510 radii: r1,
3511 },
3512 Self::Ellipse {
3513 center: c2,
3514 radii: r2,
3515 },
3516 ) => c1 == c2 && r1 == r2,
3517 (Self::Polygon { points: p1 }, Self::Polygon { points: p2 }) => {
3518 p1 == p2
3519 }
3520 (Self::Path { segments: s1 }, Self::Path { segments: s2 }) => {
3521 s1 == s2
3522 }
3523 _ => false,
3524 }
3525 }
3526}
3527impl Eq for ShapeBoundary {}
3528
3529impl ShapeBoundary {
3530 #[allow(clippy::too_many_lines)] pub fn from_css_shape(
3540 css_shape: &azul_css::shape::CssShape,
3541 reference_box: Rect,
3542 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
3543 ) -> Self {
3544 use azul_css::shape::CssShape;
3545
3546 if let Some(msgs) = debug_messages {
3547 msgs.push(LayoutDebugMessage::info(format!(
3548 "[ShapeBoundary::from_css_shape] Input CSS shape: {css_shape:?}"
3549 )));
3550 msgs.push(LayoutDebugMessage::info(format!(
3551 "[ShapeBoundary::from_css_shape] Reference box: {reference_box:?}"
3552 )));
3553 }
3554
3555 let result = match css_shape {
3556 CssShape::Circle(circle) => {
3557 let center = Point {
3558 x: reference_box.x + circle.center.x,
3559 y: reference_box.y + circle.center.y,
3560 };
3561 if let Some(msgs) = debug_messages {
3562 msgs.push(LayoutDebugMessage::info(format!(
3563 "[ShapeBoundary::from_css_shape] Circle - CSS center: ({}, {}), radius: {}",
3564 circle.center.x, circle.center.y, circle.radius
3565 )));
3566 msgs.push(LayoutDebugMessage::info(format!(
3567 "[ShapeBoundary::from_css_shape] Circle - Absolute center: ({}, {}), \
3568 radius: {}",
3569 center.x, center.y, circle.radius
3570 )));
3571 }
3572 Self::Circle {
3573 center,
3574 radius: circle.radius,
3575 }
3576 }
3577
3578 CssShape::Ellipse(ellipse) => {
3579 let center = Point {
3580 x: reference_box.x + ellipse.center.x,
3581 y: reference_box.y + ellipse.center.y,
3582 };
3583 let radii = Size {
3584 width: ellipse.radius_x,
3585 height: ellipse.radius_y,
3586 };
3587 if let Some(msgs) = debug_messages {
3588 msgs.push(LayoutDebugMessage::info(format!(
3589 "[ShapeBoundary::from_css_shape] Ellipse - center: ({}, {}), radii: ({}, \
3590 {})",
3591 center.x, center.y, radii.width, radii.height
3592 )));
3593 }
3594 Self::Ellipse { center, radii }
3595 }
3596
3597 CssShape::Polygon(polygon) => {
3598 let points = polygon
3599 .points
3600 .as_ref()
3601 .iter()
3602 .map(|pt| Point {
3603 x: reference_box.x + pt.x,
3604 y: reference_box.y + pt.y,
3605 })
3606 .collect();
3607 if let Some(msgs) = debug_messages {
3608 msgs.push(LayoutDebugMessage::info(format!(
3609 "[ShapeBoundary::from_css_shape] Polygon - {} points",
3610 polygon.points.as_ref().len()
3611 )));
3612 }
3613 Self::Polygon { points }
3614 }
3615
3616 CssShape::Inset(inset) => {
3617 let x = reference_box.x + inset.inset_left;
3619 let y = reference_box.y + inset.inset_top;
3620 let width = reference_box.width - inset.inset_left - inset.inset_right;
3621 let height = reference_box.height - inset.inset_top - inset.inset_bottom;
3622
3623 if let Some(msgs) = debug_messages {
3624 msgs.push(LayoutDebugMessage::info(format!(
3625 "[ShapeBoundary::from_css_shape] Inset - insets: ({}, {}, {}, {})",
3626 inset.inset_top, inset.inset_right, inset.inset_bottom, inset.inset_left
3627 )));
3628 msgs.push(LayoutDebugMessage::info(format!(
3629 "[ShapeBoundary::from_css_shape] Inset - resulting rect: x={x}, y={y}, \
3630 w={width}, h={height}"
3631 )));
3632 }
3633
3634 Self::Rectangle(Rect {
3635 x,
3636 y,
3637 width: width.max(0.0),
3638 height: height.max(0.0),
3639 })
3640 }
3641
3642 CssShape::Path(path) => {
3643 let segments = azul_core::path_parser::parse_svg_path_d(path.data.as_str())
3649 .map_or_else(|_| Vec::new(), |multipolygon| {
3650 flatten_svg_to_path_segments(&multipolygon, reference_box)
3651 });
3652 if let Some(msgs) = debug_messages {
3653 msgs.push(LayoutDebugMessage::info(format!(
3654 "[ShapeBoundary::from_css_shape] Path - parsed {} flattened segments",
3655 segments.len()
3656 )));
3657 }
3658 if segments.is_empty() {
3659 Self::Rectangle(reference_box)
3662 } else {
3663 Self::Path { segments }
3664 }
3665 }
3666 };
3667
3668 if let Some(msgs) = debug_messages {
3669 msgs.push(LayoutDebugMessage::info(format!(
3670 "[ShapeBoundary::from_css_shape] Result: {result:?}"
3671 )));
3672 }
3673 result
3674 }
3675}
3676
3677#[derive(Copy, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3678pub struct InlineBreak {
3679 pub break_type: BreakType,
3680 pub clear: ClearType,
3681 pub content_index: usize,
3682}
3683
3684#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3686pub enum BreakType {
3687 Soft, Hard, Page, Column, }
3692
3693#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3694pub enum ClearType {
3695 None,
3696 Left,
3697 Right,
3698 Both,
3699}
3700
3701#[derive(Debug, Clone)]
3703pub(crate) struct ShapeConstraints {
3704 pub(crate) boundaries: Vec<ShapeBoundary>,
3705 pub(crate) exclusions: Vec<ShapeBoundary>,
3706 pub(crate) writing_mode: WritingMode,
3707 pub(crate) text_align: TextAlign,
3708 pub(crate) line_height: LineHeight,
3709}
3710
3711#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
3712pub enum WritingMode {
3713 #[default]
3714 HorizontalTb, VerticalRl, VerticalLr, SidewaysRl, SidewaysLr, }
3720
3721impl WritingMode {
3722 #[must_use] pub const fn is_advance_horizontal(&self) -> bool {
3724 matches!(
3725 self,
3726 Self::HorizontalTb | Self::SidewaysRl | Self::SidewaysLr
3727 )
3728 }
3729}
3730
3731#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
3732pub enum JustifyContent {
3733 #[default]
3734 None,
3735 InterWord, InterCharacter, Distribute, Kashida, }
3740
3741#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
3743pub enum TextAlign {
3744 #[default]
3745 Left,
3746 Right,
3747 Center,
3748 Justify,
3749 Start,
3750 End, JustifyAll, }
3753
3754#[derive(Debug, Clone, Copy, PartialEq, Default, Eq, PartialOrd, Ord, Hash)]
3757pub enum TextOrientation {
3758 #[default]
3759 Mixed, Upright, Sideways, }
3763
3764#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3765#[derive(Default)]
3766pub struct TextDecoration {
3767 pub underline: bool,
3768 pub strikethrough: bool,
3769 pub overline: bool,
3770}
3771
3772
3773impl TextDecoration {
3774 #[must_use] pub fn from_css(css: azul_css::props::style::text::StyleTextDecoration) -> Self {
3780 use azul_css::props::style::text::StyleTextDecoration;
3781 match css {
3782 StyleTextDecoration::None => Self::default(),
3783 StyleTextDecoration::Underline => Self {
3784 underline: true,
3785 strikethrough: false,
3786 overline: false,
3787 },
3788 StyleTextDecoration::Overline => Self {
3789 underline: false,
3790 strikethrough: false,
3791 overline: true,
3792 },
3793 StyleTextDecoration::LineThrough => Self {
3794 underline: false,
3795 strikethrough: true,
3796 overline: false,
3797 },
3798 }
3799 }
3800}
3801
3802#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
3803pub enum TextTransform {
3804 #[default]
3805 None,
3806 Uppercase,
3807 Lowercase,
3808 Capitalize,
3809 FullWidth,
3811}
3812
3813pub type FourCc = [u8; 4];
3815
3816#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
3818pub enum Spacing {
3819 Px(i32), PxF(f32),
3825 Em(f32),
3826}
3827
3828impl Eq for Spacing {}
3832
3833impl Hash for Spacing {
3834 fn hash<H: Hasher>(&self, state: &mut H) {
3835 discriminant(self).hash(state);
3837 match self {
3838 Self::Px(val) => val.hash(state),
3839 Self::PxF(val) | Self::Em(val) => val.to_bits().hash(state),
3842 }
3843 }
3844}
3845
3846impl Default for Spacing {
3847 fn default() -> Self {
3848 Self::Px(0)
3849 }
3850}
3851
3852impl Spacing {
3853 #[allow(clippy::cast_precision_loss)] #[must_use]
3856 pub fn resolve_px(self, font_size_px: f32) -> f32 {
3857 match self {
3858 Self::Px(px) => px as f32,
3859 Self::PxF(px) => px,
3860 Self::Em(em) => em * font_size_px,
3861 }
3862 }
3863}
3864
3865impl Default for FontHash {
3866 fn default() -> Self {
3867 Self::invalid()
3868 }
3869}
3870
3871#[derive(Debug, Clone, PartialEq)]
3873pub struct StyleProperties {
3874 pub font_stack: FontStack,
3878 pub font_size_px: f32,
3879 pub color: ColorU,
3880 pub background_color: Option<ColorU>,
3891 pub background_content: Vec<StyleBackgroundContent>,
3894 pub border: Option<InlineBorderInfo>,
3896 pub letter_spacing: Spacing,
3898 pub word_spacing: Spacing,
3899
3900 pub line_height: LineHeight,
3901 pub text_decoration: TextDecoration,
3902
3903 pub font_features: Vec<String>,
3905
3906 pub font_variations: Vec<(FourCc, f32)>,
3908 pub tab_size: f32,
3910 pub text_transform: TextTransform,
3912 pub writing_mode: WritingMode,
3914 pub text_orientation: TextOrientation,
3915 pub text_combine_upright: Option<TextCombineUpright>,
3917
3918 pub font_variant_caps: FontVariantCaps,
3920 pub font_variant_numeric: FontVariantNumeric,
3921 pub font_variant_ligatures: FontVariantLigatures,
3922 pub font_variant_east_asian: FontVariantEastAsian,
3923
3924 pub vertical_align: VerticalAlign,
3929}
3930
3931impl Default for StyleProperties {
3932 fn default() -> Self {
3933 const FONT_SIZE: f32 = 16.0;
3934 const TAB_SIZE: f32 = 8.0;
3935 Self {
3936 font_stack: FontStack::default(),
3937 font_size_px: FONT_SIZE,
3938 color: ColorU::default(),
3939 background_color: None,
3940 background_content: Vec::new(),
3941 border: None,
3942 letter_spacing: Spacing::default(), word_spacing: Spacing::default(), line_height: LineHeight::Normal,
3945 text_decoration: TextDecoration::default(),
3946 font_features: Vec::new(),
3947 font_variations: Vec::new(),
3948 tab_size: TAB_SIZE, text_transform: TextTransform::default(),
3950 writing_mode: WritingMode::default(),
3951 text_orientation: TextOrientation::default(),
3952 text_combine_upright: None,
3953 font_variant_caps: FontVariantCaps::default(),
3954 font_variant_numeric: FontVariantNumeric::default(),
3955 font_variant_ligatures: FontVariantLigatures::default(),
3956 font_variant_east_asian: FontVariantEastAsian::default(),
3957 vertical_align: VerticalAlign::Baseline,
3958 }
3959 }
3960}
3961
3962impl Hash for StyleProperties {
3963 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3965 self.font_stack.hash(state);
3966 self.color.hash(state);
3967 self.background_color.hash(state);
3968 self.text_decoration.hash(state);
3969 self.font_features.hash(state);
3970 self.writing_mode.hash(state);
3971 self.text_orientation.hash(state);
3972 self.text_combine_upright.hash(state);
3973 self.vertical_align.hash(state);
3974 self.letter_spacing.hash(state);
3975 self.word_spacing.hash(state);
3976
3977 (self.font_size_px.round() as isize).hash(state);
3979 self.line_height.hash(state);
3980 }
3981}
3982
3983impl StyleProperties {
3984 #[allow(clippy::cast_possible_truncation)] #[must_use] pub fn layout_hash(&self) -> u64 {
4004 use std::hash::Hasher;
4005 let mut hasher = DefaultHasher::new();
4006
4007 self.font_stack.hash(&mut hasher);
4009 self.font_size_px.to_bits().hash(&mut hasher);
4013 self.font_features.hash(&mut hasher);
4014 for (tag, value) in &self.font_variations {
4016 tag.hash(&mut hasher);
4017 (value.round() as i32).hash(&mut hasher);
4018 }
4019
4020 self.letter_spacing.hash(&mut hasher);
4022 self.word_spacing.hash(&mut hasher);
4023 self.line_height.hash(&mut hasher);
4024 (self.tab_size.round() as isize).hash(&mut hasher);
4025
4026 self.writing_mode.hash(&mut hasher);
4028 self.text_orientation.hash(&mut hasher);
4029 self.text_combine_upright.hash(&mut hasher);
4030
4031 self.text_transform.hash(&mut hasher);
4033
4034 self.font_variant_caps.hash(&mut hasher);
4036 self.font_variant_numeric.hash(&mut hasher);
4037 self.font_variant_ligatures.hash(&mut hasher);
4038 self.font_variant_east_asian.hash(&mut hasher);
4039
4040 hasher.finish()
4041 }
4042
4043 #[must_use] pub fn layout_eq(&self, other: &Self) -> bool {
4052 self.layout_hash() == other.layout_hash()
4053 }
4054}
4055
4056#[derive(Copy, Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
4057pub enum TextCombineUpright {
4058 None,
4059 All, Digits(u8), }
4062
4063#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4064pub enum GlyphSource {
4065 Char,
4067 Hyphen,
4069}
4070
4071#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4072pub enum CharacterClass {
4073 Space, Punctuation, Letter, Ideograph, Symbol, Combining, }
4080
4081#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4082pub enum GlyphOrientation {
4083 Horizontal, Vertical, Upright, Mixed, }
4088
4089#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4091pub enum BidiDirection {
4092 Ltr,
4093 Rtl,
4094}
4095
4096impl BidiDirection {
4097 #[must_use] pub const fn is_rtl(&self) -> bool {
4098 matches!(self, Self::Rtl)
4099 }
4100}
4101
4102#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4108#[derive(Default)]
4109pub enum UnicodeBidi {
4110 #[default]
4111 Normal,
4112 Embed,
4113 Isolate,
4114 BidiOverride,
4115 IsolateOverride,
4116 Plaintext,
4117}
4118
4119
4120#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
4121pub enum FontVariantCaps {
4122 #[default]
4123 Normal,
4124 SmallCaps,
4125 AllSmallCaps,
4126 PetiteCaps,
4127 AllPetiteCaps,
4128 Unicase,
4129 TitlingCaps,
4130}
4131
4132#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
4133pub enum FontVariantNumeric {
4134 #[default]
4135 Normal,
4136 LiningNums,
4137 OldstyleNums,
4138 ProportionalNums,
4139 TabularNums,
4140 DiagonalFractions,
4141 StackedFractions,
4142 Ordinal,
4143 SlashedZero,
4144}
4145
4146#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
4147pub enum FontVariantLigatures {
4148 #[default]
4149 Normal,
4150 None,
4151 Common,
4152 NoCommon,
4153 Discretionary,
4154 NoDiscretionary,
4155 Historical,
4156 NoHistorical,
4157 Contextual,
4158 NoContextual,
4159}
4160
4161#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
4162pub enum FontVariantEastAsian {
4163 #[default]
4164 Normal,
4165 Jis78,
4166 Jis83,
4167 Jis90,
4168 Jis04,
4169 Simplified,
4170 Traditional,
4171 FullWidth,
4172 ProportionalWidth,
4173 Ruby,
4174}
4175
4176#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4177pub struct BidiLevel(u8);
4178
4179impl BidiLevel {
4180 #[must_use] pub const fn new(level: u8) -> Self {
4181 Self(level)
4182 }
4183 #[must_use] pub const fn is_rtl(&self) -> bool {
4184 self.0 % 2 == 1
4185 }
4186 #[must_use] pub const fn level(&self) -> u8 {
4187 self.0
4188 }
4189}
4190
4191#[derive(Debug, Clone)]
4193pub struct StyleOverride {
4194 pub target: ContentIndex,
4196 pub style: PartialStyleProperties,
4199}
4200
4201#[derive(Debug, Clone, Default)]
4202pub struct PartialStyleProperties {
4203 pub font_stack: Option<FontStack>,
4204 pub font_size_px: Option<f32>,
4205 pub color: Option<ColorU>,
4206 pub letter_spacing: Option<Spacing>,
4207 pub word_spacing: Option<Spacing>,
4208 pub line_height: Option<LineHeight>,
4209 pub text_decoration: Option<TextDecoration>,
4210 pub font_features: Option<Vec<String>>,
4211 pub font_variations: Option<Vec<(FourCc, f32)>>,
4212 pub tab_size: Option<f32>,
4213 pub text_transform: Option<TextTransform>,
4214 pub writing_mode: Option<WritingMode>,
4215 pub text_orientation: Option<TextOrientation>,
4216 pub text_combine_upright: Option<Option<TextCombineUpright>>,
4217 pub font_variant_caps: Option<FontVariantCaps>,
4218 pub font_variant_numeric: Option<FontVariantNumeric>,
4219 pub font_variant_ligatures: Option<FontVariantLigatures>,
4220 pub font_variant_east_asian: Option<FontVariantEastAsian>,
4221}
4222
4223impl Hash for PartialStyleProperties {
4224 fn hash<H: Hasher>(&self, state: &mut H) {
4225 self.font_stack.hash(state);
4226 self.font_size_px.map(f32::to_bits).hash(state);
4227 self.color.hash(state);
4228 self.letter_spacing.hash(state);
4229 self.word_spacing.hash(state);
4230 self.line_height.hash(state);
4231 self.text_decoration.hash(state);
4232 self.font_features.hash(state);
4233
4234 if let Some(v) = self.font_variations.as_ref() {
4236 for (tag, val) in v {
4237 tag.hash(state);
4238 val.to_bits().hash(state);
4239 }
4240 }
4241
4242 self.tab_size.map(f32::to_bits).hash(state);
4243 self.text_transform.hash(state);
4244 self.writing_mode.hash(state);
4245 self.text_orientation.hash(state);
4246 self.text_combine_upright.hash(state);
4247 self.font_variant_caps.hash(state);
4248 self.font_variant_numeric.hash(state);
4249 self.font_variant_ligatures.hash(state);
4250 self.font_variant_east_asian.hash(state);
4251 }
4252}
4253
4254impl PartialEq for PartialStyleProperties {
4255 fn eq(&self, other: &Self) -> bool {
4256 self.font_stack == other.font_stack &&
4257 self.font_size_px.map(f32::to_bits) == other.font_size_px.map(f32::to_bits) &&
4258 self.color == other.color &&
4259 self.letter_spacing == other.letter_spacing &&
4260 self.word_spacing == other.word_spacing &&
4261 self.line_height == other.line_height &&
4262 self.text_decoration == other.text_decoration &&
4263 self.font_features == other.font_features &&
4264 self.font_variations == other.font_variations && self.tab_size.map(f32::to_bits) == other.tab_size.map(f32::to_bits) &&
4266 self.text_transform == other.text_transform &&
4267 self.writing_mode == other.writing_mode &&
4268 self.text_orientation == other.text_orientation &&
4269 self.text_combine_upright == other.text_combine_upright &&
4270 self.font_variant_caps == other.font_variant_caps &&
4271 self.font_variant_numeric == other.font_variant_numeric &&
4272 self.font_variant_ligatures == other.font_variant_ligatures &&
4273 self.font_variant_east_asian == other.font_variant_east_asian
4274 }
4275}
4276
4277impl Eq for PartialStyleProperties {}
4278
4279impl StyleProperties {
4280 fn apply_override(&self, partial: &PartialStyleProperties) -> Self {
4281 let mut new_style = self.clone();
4282 if let Some(val) = &partial.font_stack {
4283 new_style.font_stack = val.clone();
4284 }
4285 if let Some(val) = partial.font_size_px {
4286 new_style.font_size_px = val;
4287 }
4288 if let Some(val) = &partial.color {
4289 new_style.color = *val;
4290 }
4291 if let Some(val) = partial.letter_spacing {
4292 new_style.letter_spacing = val;
4293 }
4294 if let Some(val) = partial.word_spacing {
4295 new_style.word_spacing = val;
4296 }
4297 if let Some(val) = partial.line_height {
4298 new_style.line_height = val;
4299 }
4300 if let Some(val) = &partial.text_decoration {
4301 new_style.text_decoration = *val;
4302 }
4303 if let Some(val) = &partial.font_features {
4304 new_style.font_features.clone_from(val);
4305 }
4306 if let Some(val) = &partial.font_variations {
4307 new_style.font_variations.clone_from(val);
4308 }
4309 if let Some(val) = partial.tab_size {
4310 new_style.tab_size = val;
4311 }
4312 if let Some(val) = partial.text_transform {
4313 new_style.text_transform = val;
4314 }
4315 if let Some(val) = partial.writing_mode {
4316 new_style.writing_mode = val;
4317 }
4318 if let Some(val) = partial.text_orientation {
4319 new_style.text_orientation = val;
4320 }
4321 if let Some(val) = &partial.text_combine_upright {
4322 new_style.text_combine_upright.clone_from(val);
4323 }
4324 if let Some(val) = partial.font_variant_caps {
4325 new_style.font_variant_caps = val;
4326 }
4327 if let Some(val) = partial.font_variant_numeric {
4328 new_style.font_variant_numeric = val;
4329 }
4330 if let Some(val) = partial.font_variant_ligatures {
4331 new_style.font_variant_ligatures = val;
4332 }
4333 if let Some(val) = partial.font_variant_east_asian {
4334 new_style.font_variant_east_asian = val;
4335 }
4336 new_style
4337 }
4338}
4339
4340#[derive(Debug, Clone, Copy, PartialEq)]
4342pub enum GlyphKind {
4343 Character,
4345 Hyphen,
4347 NotDef,
4349 Kashida {
4351 width: f32,
4353 },
4354}
4355
4356#[derive(Debug, Clone)]
4363#[repr(C, u8)]
4364pub enum LogicalItem {
4365 Text {
4366 source: ContentIndex,
4368 text: String,
4370 style: Arc<StyleProperties>,
4371 marker_position_outside: Option<bool>,
4375 source_node_id: Option<NodeId>,
4378 },
4379 CombinedText {
4382 source: ContentIndex,
4383 text: String,
4384 style: Arc<StyleProperties>,
4385 },
4386 Ruby {
4387 source: ContentIndex,
4388 base_text: String,
4391 ruby_text: String,
4392 style: Arc<StyleProperties>,
4393 },
4394 Object {
4395 source: ContentIndex,
4397 content: InlineContent,
4399 },
4400 Tab {
4401 source: ContentIndex,
4402 style: Arc<StyleProperties>,
4403 },
4404 Break {
4405 source: ContentIndex,
4406 break_info: InlineBreak,
4407 },
4408}
4409
4410impl Hash for LogicalItem {
4411 fn hash<H: Hasher>(&self, state: &mut H) {
4412 discriminant(self).hash(state);
4413 match self {
4414 Self::Text {
4415 source,
4416 text,
4417 style,
4418 marker_position_outside,
4419 source_node_id,
4420 } => {
4421 source.hash(state);
4422 text.hash(state);
4423 style.as_ref().hash(state); marker_position_outside.hash(state);
4425 source_node_id.hash(state);
4426 }
4427 Self::CombinedText {
4428 source,
4429 text,
4430 style,
4431 } => {
4432 source.hash(state);
4433 text.hash(state);
4434 style.as_ref().hash(state);
4435 }
4436 Self::Ruby {
4437 source,
4438 base_text,
4439 ruby_text,
4440 style,
4441 } => {
4442 source.hash(state);
4443 base_text.hash(state);
4444 ruby_text.hash(state);
4445 style.as_ref().hash(state);
4446 }
4447 Self::Object { source, content } => {
4448 source.hash(state);
4449 content.hash(state);
4450 }
4451 Self::Tab { source, style } => {
4452 source.hash(state);
4453 style.as_ref().hash(state);
4454 }
4455 Self::Break { source, break_info } => {
4456 source.hash(state);
4457 break_info.hash(state);
4458 }
4459 }
4460 }
4461}
4462
4463#[derive(Debug, Clone)]
4466pub struct VisualItem {
4467 pub logical_source: LogicalItem,
4470 pub bidi_level: BidiLevel,
4472 pub script: Script,
4474 pub text: String,
4476 pub run_byte_offset: usize,
4482}
4483
4484#[derive(Debug, Clone)]
4491#[repr(C, u8)]
4492pub enum ShapedItem {
4493 Cluster(ShapedCluster),
4494 CombinedBlock {
4497 source: ContentIndex,
4498 glyphs: ShapedGlyphVec,
4500 bounds: Rect,
4501 baseline_offset: f32,
4502 },
4503 Object {
4504 source: ContentIndex,
4505 bounds: Rect,
4506 baseline_offset: f32,
4507 content: InlineContent,
4509 },
4510 Tab {
4511 source: ContentIndex,
4512 bounds: Rect,
4513 },
4514 Break {
4515 source: ContentIndex,
4516 break_info: InlineBreak,
4517 },
4518}
4519
4520impl ShapedItem {
4521 #[must_use] pub const fn as_cluster(&self) -> Option<&ShapedCluster> {
4522 match self {
4523 Self::Cluster(c) => Some(c),
4524 _ => None,
4525 }
4526 }
4527 #[allow(clippy::match_same_arms)] #[must_use] pub fn bounds(&self) -> Rect {
4534 match self {
4535 Self::Cluster(cluster) => {
4536 let width = cluster.advance;
4538
4539 let (ascent, descent) = get_item_vertical_metrics_approx(self);
4543 let height = ascent + descent;
4544
4545 Rect {
4546 x: 0.0,
4547 y: 0.0,
4548 width,
4549 height,
4550 }
4551 }
4552 Self::CombinedBlock { bounds, .. } => *bounds,
4555 Self::Object { bounds, .. } => *bounds,
4556 Self::Tab { bounds, .. } => *bounds,
4557
4558 Self::Break { .. } => Rect::default(), }
4561 }
4562}
4563
4564#[derive(Debug, Clone)]
4566pub struct ShapedCluster {
4567 pub text: String,
4570 pub source_cluster_id: GraphemeClusterId,
4572 pub source_content_index: ContentIndex,
4574 pub source_node_id: Option<NodeId>,
4577 pub glyphs: ShapedGlyphVec,
4581 pub advance: f32,
4583 pub direction: BidiDirection,
4585 pub style: Arc<StyleProperties>,
4587 pub marker_position_outside: Option<bool>,
4591 pub is_first_fragment: bool,
4596 pub is_last_fragment: bool,
4599}
4600
4601#[derive(Debug, Clone)]
4603pub struct ShapedGlyph {
4604 pub kind: GlyphKind,
4606 pub glyph_id: u16,
4608 pub cluster_offset: u32,
4610 pub advance: f32,
4613 pub kerning: f32,
4616 pub offset: Point,
4618 pub vertical_advance: f32,
4620 pub vertical_offset: Point,
4622 pub script: Script,
4623 pub style: Arc<StyleProperties>,
4624 pub font_hash: u64,
4626 pub font_metrics: LayoutFontMetrics,
4628}
4629
4630impl ShapedGlyph {
4631 #[must_use] pub fn into_glyph_instance<T: ParsedFontTrait>(
4632 &self,
4633 writing_mode: WritingMode,
4634 loaded_fonts: &LoadedFonts<T>,
4635 ) -> GlyphInstance {
4636 let size = loaded_fonts
4637 .get_by_hash(self.font_hash)
4638 .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
4639 .unwrap_or_default();
4640
4641 let position = if writing_mode.is_advance_horizontal() {
4642 LogicalPosition {
4643 x: self.offset.x,
4644 y: self.offset.y,
4645 }
4646 } else {
4647 LogicalPosition {
4648 x: self.vertical_offset.x,
4649 y: self.vertical_offset.y,
4650 }
4651 };
4652
4653 GlyphInstance {
4654 index: u32::from(self.glyph_id),
4655 point: position,
4656 size,
4657 }
4658 }
4659
4660 #[must_use] pub fn into_glyph_instance_at<T: ParsedFontTrait>(
4663 &self,
4664 writing_mode: WritingMode,
4665 absolute_position: LogicalPosition,
4666 loaded_fonts: &LoadedFonts<T>,
4667 ) -> GlyphInstance {
4668 let size = loaded_fonts
4669 .get_by_hash(self.font_hash)
4670 .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
4671 .unwrap_or_default();
4672
4673 GlyphInstance {
4674 index: u32::from(self.glyph_id),
4675 point: absolute_position,
4676 size,
4677 }
4678 }
4679
4680 #[must_use] pub fn into_glyph_instance_at_simple(
4684 &self,
4685 _writing_mode: WritingMode,
4686 absolute_position: LogicalPosition,
4687 ) -> GlyphInstance {
4688 GlyphInstance {
4691 index: u32::from(self.glyph_id),
4692 point: absolute_position,
4693 size: LogicalSize::default(),
4694 }
4695 }
4696}
4697
4698#[derive(Debug, Clone)]
4701pub struct PositionedItem {
4702 pub item: ShapedItem,
4703 pub position: Point,
4704 pub line_index: usize,
4705}
4706
4707#[derive(Debug, Clone)]
4708pub struct UnifiedLayout {
4709 pub items: Vec<PositionedItem>,
4710 pub overflow: OverflowInfo,
4712}
4713
4714impl UnifiedLayout {
4715 #[must_use] pub fn bounds(&self) -> Rect {
4718 if self.items.is_empty() {
4719 return Rect::default();
4720 }
4721
4722 let mut min_x = f32::MAX;
4723 let mut min_y = f32::MAX;
4724 let mut max_x = f32::MIN;
4725 let mut max_y = f32::MIN;
4726
4727 for item in &self.items {
4728 let item_x = item.position.x;
4729 let item_y = item.position.y;
4730
4731 let item_bounds = item.item.bounds();
4733 let item_width = item_bounds.width;
4734 let item_height = item_bounds.height;
4735
4736 min_x = min_x.min(item_x);
4737 min_y = min_y.min(item_y);
4738 max_x = max_x.max(item_x + item_width);
4739 max_y = max_y.max(item_y + item_height);
4740 }
4741
4742 Rect {
4743 x: min_x,
4744 y: min_y,
4745 width: max_x - min_x,
4746 height: max_y - min_y,
4747 }
4748 }
4749
4750 #[must_use] pub const fn is_empty(&self) -> bool {
4751 self.items.is_empty()
4752 }
4753 #[must_use] pub fn first_baseline(&self) -> Option<f32> {
4754 self.items
4755 .iter()
4756 .find_map(|item| get_baseline_for_item(&item.item))
4757 }
4758
4759 #[must_use] pub fn last_baseline(&self) -> Option<f32> {
4760 self.items
4761 .iter()
4762 .rev()
4763 .find_map(|item| get_baseline_for_item(&item.item))
4764 }
4765
4766 #[allow(clippy::suboptimal_flops)] #[must_use] pub fn hittest_cursor(&self, point: LogicalPosition) -> Option<TextCursor> {
4773 if self.items.is_empty() {
4774 return None;
4775 }
4776
4777 let mut closest_item_idx = 0;
4779 let mut closest_distance = f32::MAX;
4780
4781 for (idx, item) in self.items.iter().enumerate() {
4782 if !matches!(item.item, ShapedItem::Cluster(_)) {
4784 continue;
4785 }
4786
4787 let item_bounds = item.item.bounds();
4788 let item_center_y = item.position.y + item_bounds.height / 2.0;
4789
4790 let vertical_distance = (point.y - item_center_y).abs();
4792
4793 let horizontal_distance = if point.x < item.position.x {
4795 item.position.x - point.x
4796 } else if point.x > item.position.x + item_bounds.width {
4797 point.x - (item.position.x + item_bounds.width)
4798 } else {
4799 0.0 };
4801
4802 let distance = vertical_distance * 2.0 + horizontal_distance;
4804
4805 if distance < closest_distance {
4806 closest_distance = distance;
4807 closest_item_idx = idx;
4808 }
4809 }
4810
4811 let closest_item = &self.items[closest_item_idx];
4813 let cluster = match &closest_item.item {
4814 ShapedItem::Cluster(c) => c,
4815 ShapedItem::Object { source, .. } | ShapedItem::CombinedBlock { source, .. } => {
4817 return Some(TextCursor {
4818 cluster_id: GraphemeClusterId {
4819 source_run: source.run_index,
4820 start_byte_in_run: source.item_index,
4821 },
4822 affinity: if point.x
4823 < closest_item.position.x + (closest_item.item.bounds().width / 2.0)
4824 {
4825 CursorAffinity::Leading
4826 } else {
4827 CursorAffinity::Trailing
4828 },
4829 });
4830 }
4831 _ => return None,
4832 };
4833
4834 let cluster_mid_x = closest_item.position.x + cluster.advance / 2.0;
4836 let affinity = if point.x < cluster_mid_x {
4837 CursorAffinity::Leading
4838 } else {
4839 CursorAffinity::Trailing
4840 };
4841
4842 Some(TextCursor {
4843 cluster_id: cluster.source_cluster_id,
4844 affinity,
4845 })
4846 }
4847
4848 #[allow(clippy::too_many_lines)] #[must_use] pub fn get_selection_rects(&self, range: &SelectionRange) -> Vec<LogicalRect> {
4852 let mut cluster_map: HashMap<GraphemeClusterId, &PositionedItem> = HashMap::new();
4854 for item in &self.items {
4855 if let Some(cluster) = item.item.as_cluster() {
4856 cluster_map.insert(cluster.source_cluster_id, item);
4857 }
4858 }
4859
4860 let (start_cursor, end_cursor) = if range.start.cluster_id > range.end.cluster_id
4862 || (range.start.cluster_id == range.end.cluster_id
4863 && range.start.affinity > range.end.affinity)
4864 {
4865 (range.end, range.start)
4866 } else {
4867 (range.start, range.end)
4868 };
4869
4870 let Some(start_item) = cluster_map.get(&start_cursor.cluster_id) else {
4872 return Vec::new();
4873 };
4874 let Some(end_item) = cluster_map.get(&end_cursor.cluster_id) else {
4875 return Vec::new();
4876 };
4877
4878 let mut rects = Vec::new();
4879
4880 let get_cursor_x = |item: &PositionedItem, affinity: CursorAffinity| -> f32 {
4884 let left = item.position.x;
4885 let right = item.position.x + get_item_measure(&item.item, false);
4886 let rtl = item.item.as_cluster().is_some_and(|c| c.direction.is_rtl());
4887 match (affinity, rtl) {
4888 (CursorAffinity::Leading, false) | (CursorAffinity::Trailing, true) => left,
4889 (CursorAffinity::Trailing, false) | (CursorAffinity::Leading, true) => right,
4890 }
4891 };
4892
4893 let get_line_bounds = |line_index: usize| -> Option<LogicalRect> {
4895 let items_on_line = self.items.iter().filter(|i| i.line_index == line_index);
4896
4897 let mut min_x: Option<f32> = None;
4898 let mut max_x: Option<f32> = None;
4899 let mut min_y: Option<f32> = None;
4900 let mut max_y: Option<f32> = None;
4901
4902 for item in items_on_line {
4903 let item_bounds = item.item.bounds();
4905 if item_bounds.width <= 0.0 && item_bounds.height <= 0.0 {
4906 continue;
4907 }
4908
4909 let item_x_end = item.position.x + item_bounds.width;
4910 let item_y_end = item.position.y + item_bounds.height;
4911
4912 min_x = Some(min_x.map_or(item.position.x, |mx| mx.min(item.position.x)));
4913 max_x = Some(max_x.map_or(item_x_end, |mx| mx.max(item_x_end)));
4914 min_y = Some(min_y.map_or(item.position.y, |my| my.min(item.position.y)));
4915 max_y = Some(max_y.map_or(item_y_end, |my| my.max(item_y_end)));
4916 }
4917
4918 if let (Some(min_x), Some(max_x), Some(min_y), Some(max_y)) =
4919 (min_x, max_x, min_y, max_y)
4920 {
4921 Some(LogicalRect {
4922 origin: LogicalPosition { x: min_x, y: min_y },
4923 size: LogicalSize {
4924 width: max_x - min_x,
4925 height: max_y - min_y,
4926 },
4927 })
4928 } else {
4929 None
4930 }
4931 };
4932
4933 if start_item.line_index == end_item.line_index {
4935 if let Some(line_bounds) = get_line_bounds(start_item.line_index) {
4936 let mut segments: Vec<(f32, f32, BidiDirection)> = Vec::new();
4943 for item in &self.items {
4944 if item.line_index != start_item.line_index {
4945 continue;
4946 }
4947 let Some(c) = item.item.as_cluster() else {
4948 continue;
4949 };
4950 let id = c.source_cluster_id;
4951 let after_start = id > start_cursor.cluster_id
4956 || (id == start_cursor.cluster_id
4957 && start_cursor.affinity == CursorAffinity::Leading);
4958 let before_end = id < end_cursor.cluster_id
4959 || (id == end_cursor.cluster_id
4960 && end_cursor.affinity == CursorAffinity::Trailing);
4961 if !(after_start && before_end) {
4962 continue;
4963 }
4964 let x0 = item.position.x;
4965 let x1 = item.position.x + get_item_measure(&item.item, false);
4966 let (lo, hi) = (x0.min(x1), x0.max(x1));
4967 if let Some(last) = segments.last_mut() {
4968 let contiguous = lo <= last.1 + 0.5 && hi >= last.0 - 0.5;
4969 if last.2 == c.direction && contiguous {
4970 last.0 = last.0.min(lo);
4971 last.1 = last.1.max(hi);
4972 continue;
4973 }
4974 }
4975 segments.push((lo, hi, c.direction));
4976 }
4977
4978 if segments.is_empty() {
4979 let start_x = get_cursor_x(start_item, start_cursor.affinity);
4982 let end_x = get_cursor_x(end_item, end_cursor.affinity);
4983 rects.push(LogicalRect {
4984 origin: LogicalPosition {
4985 x: start_x.min(end_x),
4986 y: line_bounds.origin.y,
4987 },
4988 size: LogicalSize {
4989 width: (end_x - start_x).abs(),
4990 height: line_bounds.size.height,
4991 },
4992 });
4993 } else {
4994 for (lo, hi, _dir) in segments {
4995 rects.push(LogicalRect {
4996 origin: LogicalPosition {
4997 x: lo,
4998 y: line_bounds.origin.y,
4999 },
5000 size: LogicalSize {
5001 width: hi - lo,
5002 height: line_bounds.size.height,
5003 },
5004 });
5005 }
5006 }
5007 }
5008 }
5009 else {
5011 if let Some(start_line_bounds) = get_line_bounds(start_item.line_index) {
5015 let start_x = get_cursor_x(start_item, start_cursor.affinity);
5016 let line_left = start_line_bounds.origin.x;
5017 let line_right = start_line_bounds.origin.x + start_line_bounds.size.width;
5018 let rtl = start_item.item.as_cluster().is_some_and(|c| c.direction.is_rtl());
5019 let (lo, hi) = if rtl { (line_left, start_x) } else { (start_x, line_right) };
5020 rects.push(LogicalRect {
5021 origin: LogicalPosition {
5022 x: lo,
5023 y: start_line_bounds.origin.y,
5024 },
5025 size: LogicalSize {
5026 width: hi - lo,
5027 height: start_line_bounds.size.height,
5028 },
5029 });
5030 }
5031
5032 for line_idx in (start_item.line_index + 1)..end_item.line_index {
5034 if let Some(line_bounds) = get_line_bounds(line_idx) {
5035 rects.push(line_bounds);
5036 }
5037 }
5038
5039 if let Some(end_line_bounds) = get_line_bounds(end_item.line_index) {
5043 let end_x = get_cursor_x(end_item, end_cursor.affinity);
5044 let line_left = end_line_bounds.origin.x;
5045 let line_right = end_line_bounds.origin.x + end_line_bounds.size.width;
5046 let rtl = end_item.item.as_cluster().is_some_and(|c| c.direction.is_rtl());
5047 let (lo, hi) = if rtl { (end_x, line_right) } else { (line_left, end_x) };
5048 rects.push(LogicalRect {
5049 origin: LogicalPosition {
5050 x: lo,
5051 y: end_line_bounds.origin.y,
5052 },
5053 size: LogicalSize {
5054 width: hi - lo,
5055 height: end_line_bounds.size.height,
5056 },
5057 });
5058 }
5059 }
5060
5061 rects
5062 }
5063
5064 #[must_use] pub fn get_cursor_rect(&self, cursor: &TextCursor) -> Option<LogicalRect> {
5066 let mut last_cluster: Option<(&PositionedItem, &ShapedCluster)> = None;
5068 for item in &self.items {
5069 if let ShapedItem::Cluster(cluster) = &item.item {
5070 if cluster.source_cluster_id == cursor.cluster_id {
5071 let line_height = item.item.bounds().height;
5073 let (lead_x, trail_x) = if cluster.direction.is_rtl() {
5077 (item.position.x + cluster.advance, item.position.x)
5078 } else {
5079 (item.position.x, item.position.x + cluster.advance)
5080 };
5081 let cursor_x = match cursor.affinity {
5082 CursorAffinity::Leading => lead_x,
5083 CursorAffinity::Trailing => trail_x,
5084 };
5085 return Some(LogicalRect {
5086 origin: LogicalPosition {
5087 x: cursor_x,
5088 y: item.position.y,
5089 },
5090 size: LogicalSize {
5091 width: 1.0,
5092 height: line_height,
5093 },
5094 });
5095 }
5096 last_cluster = Some((item, cluster));
5097 }
5098 }
5099 if let Some((item, cluster)) = last_cluster {
5101 if cursor.cluster_id.source_run == cluster.source_cluster_id.source_run
5102 && cursor.cluster_id.start_byte_in_run >= cluster.source_cluster_id.start_byte_in_run
5103 {
5104 let line_height = item.item.bounds().height;
5105 let past_end_x = if cluster.direction.is_rtl() {
5108 item.position.x
5109 } else {
5110 item.position.x + cluster.advance
5111 };
5112 return Some(LogicalRect {
5113 origin: LogicalPosition {
5114 x: past_end_x,
5115 y: item.position.y,
5116 },
5117 size: LogicalSize {
5118 width: 1.0,
5119 height: line_height,
5120 },
5121 });
5122 }
5123 }
5124 None
5125 }
5126
5127 #[must_use] pub fn get_first_cluster_cursor(&self) -> Option<TextCursor> {
5129 for item in &self.items {
5130 if let ShapedItem::Cluster(cluster) = &item.item {
5131 return Some(TextCursor {
5132 cluster_id: cluster.source_cluster_id,
5133 affinity: CursorAffinity::Leading,
5134 });
5135 }
5136 }
5137 None
5138 }
5139
5140 #[must_use] pub fn get_last_cluster_cursor(&self) -> Option<TextCursor> {
5142 for item in self.items.iter().rev() {
5143 if let ShapedItem::Cluster(cluster) = &item.item {
5144 return Some(TextCursor {
5145 cluster_id: cluster.source_cluster_id,
5146 affinity: CursorAffinity::Trailing,
5147 });
5148 }
5149 }
5150 None
5151 }
5152
5153 fn grapheme_stops(&self) -> Vec<GraphemeClusterId> {
5159 let mut stops: Vec<(GraphemeClusterId, &str)> = self
5160 .items
5161 .iter()
5162 .filter_map(|it| {
5163 it.item
5164 .as_cluster()
5165 .map(|c| (c.source_cluster_id, c.text.as_str()))
5166 })
5167 .collect();
5168 stops.sort_by(|a, b| {
5169 (a.0.source_run, a.0.start_byte_in_run).cmp(&(b.0.source_run, b.0.start_byte_in_run))
5170 });
5171 stops.dedup_by_key(|(id, _)| *id);
5172 stops
5173 .into_iter()
5174 .filter(|(_, text)| !Self::cluster_is_grapheme_continuation(text))
5175 .map(|(id, _)| id)
5176 .collect()
5177 }
5178
5179 fn cluster_is_grapheme_continuation(text: &str) -> bool {
5183 let Some(first) = text.chars().next() else {
5184 return false;
5185 };
5186 let mut probe = String::with_capacity(1 + first.len_utf8());
5189 probe.push('x');
5190 probe.push(first);
5191 probe.graphemes(true).count() == 1
5192 }
5193
5194 fn grapheme_caret_offset(stops: &[GraphemeClusterId], cursor: &TextCursor) -> Option<usize> {
5199 let trailing = usize::from(cursor.affinity == CursorAffinity::Trailing);
5200 if let Some(idx) = stops.iter().position(|id| *id == cursor.cluster_id) {
5201 return Some(idx + trailing);
5202 }
5203 let key = (cursor.cluster_id.source_run, cursor.cluster_id.start_byte_in_run);
5204 let idx = stops
5205 .iter()
5206 .rposition(|id| (id.source_run, id.start_byte_in_run) <= key)?;
5207 Some(idx + trailing)
5208 }
5209
5210 fn cursor_from_grapheme_offset(stops: &[GraphemeClusterId], offset: usize) -> TextCursor {
5214 let n = stops.len();
5215 if offset >= n {
5216 TextCursor { cluster_id: stops[n - 1], affinity: CursorAffinity::Trailing }
5217 } else {
5218 TextCursor { cluster_id: stops[offset], affinity: CursorAffinity::Leading }
5219 }
5220 }
5221
5222 pub fn move_cursor_left(
5227 &self,
5228 cursor: TextCursor,
5229 debug: &mut Option<Vec<String>>,
5230 ) -> TextCursor {
5231 let stops = self.grapheme_stops();
5232 if stops.is_empty() {
5233 return cursor;
5234 }
5235 let Some(offset) = Self::grapheme_caret_offset(&stops, &cursor) else {
5236 return cursor;
5237 };
5238 let moved = Self::cursor_from_grapheme_offset(&stops, offset.saturating_sub(1));
5239 if let Some(d) = debug {
5240 d.push(format!(
5241 "[Cursor] move_cursor_left: byte {} -> byte {}",
5242 cursor.cluster_id.start_byte_in_run, moved.cluster_id.start_byte_in_run
5243 ));
5244 }
5245 moved
5246 }
5247
5248 pub fn move_cursor_right(
5253 &self,
5254 cursor: TextCursor,
5255 debug: &mut Option<Vec<String>>,
5256 ) -> TextCursor {
5257 let stops = self.grapheme_stops();
5258 if stops.is_empty() {
5259 return cursor;
5260 }
5261 let Some(offset) = Self::grapheme_caret_offset(&stops, &cursor) else {
5262 return cursor;
5263 };
5264 let moved = Self::cursor_from_grapheme_offset(&stops, (offset + 1).min(stops.len()));
5265 if let Some(d) = debug {
5266 d.push(format!(
5267 "[Cursor] move_cursor_right: byte {} -> byte {}",
5268 cursor.cluster_id.start_byte_in_run, moved.cluster_id.start_byte_in_run
5269 ));
5270 }
5271 moved
5272 }
5273
5274 pub fn move_cursor_up(
5276 &self,
5277 cursor: TextCursor,
5278 goal_x: &mut Option<f32>,
5279 debug: &mut Option<Vec<String>>,
5280 ) -> TextCursor {
5281 if let Some(d) = debug {
5282 d.push(format!(
5283 "[Cursor] move_cursor_up: from byte {} (affinity {:?})",
5284 cursor.cluster_id.start_byte_in_run, cursor.affinity
5285 ));
5286 }
5287
5288 let Some(current_item) = self.items.iter().find(|i| {
5289 i.item
5290 .as_cluster()
5291 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5292 }) else {
5293 if let Some(d) = debug {
5294 d.push(format!(
5295 "[Cursor] move_cursor_up: cursor not found in items, staying at byte {}",
5296 cursor.cluster_id.start_byte_in_run
5297 ));
5298 }
5299 return cursor;
5300 };
5301
5302 if let Some(d) = debug {
5303 d.push(format!(
5304 "[Cursor] move_cursor_up: current line {}, position ({}, {})",
5305 current_item.line_index, current_item.position.x, current_item.position.y
5306 ));
5307 }
5308
5309 let target_line_idx = current_item.line_index.saturating_sub(1);
5310 if current_item.line_index == target_line_idx {
5311 if let Some(d) = debug {
5312 d.push(format!(
5313 "[Cursor] move_cursor_up: already at top line {}, staying put",
5314 current_item.line_index
5315 ));
5316 }
5317 return cursor;
5318 }
5319
5320 let current_x = goal_x.unwrap_or_else(|| {
5321 let x = match cursor.affinity {
5322 CursorAffinity::Leading => current_item.position.x,
5323 CursorAffinity::Trailing => {
5324 current_item.position.x + get_item_measure(¤t_item.item, false)
5325 }
5326 };
5327 *goal_x = Some(x);
5328 x
5329 });
5330
5331 let target_y = self
5333 .items
5334 .iter()
5335 .find(|i| i.line_index == target_line_idx)
5336 .map_or(current_item.position.y, |i| i.position.y + (i.item.bounds().height / 2.0));
5337
5338 if let Some(d) = debug {
5339 d.push(format!(
5340 "[Cursor] move_cursor_up: target line {target_line_idx}, hittesting at ({current_x}, {target_y})"
5341 ));
5342 }
5343
5344 let result = self
5345 .hittest_cursor(LogicalPosition {
5346 x: current_x,
5347 y: target_y,
5348 })
5349 .unwrap_or(cursor);
5350
5351 if let Some(d) = debug {
5352 d.push(format!(
5353 "[Cursor] move_cursor_up: result byte {} (affinity {:?})",
5354 result.cluster_id.start_byte_in_run, result.affinity
5355 ));
5356 }
5357
5358 result
5359 }
5360
5361 pub fn move_cursor_down(
5363 &self,
5364 cursor: TextCursor,
5365 goal_x: &mut Option<f32>,
5366 debug: &mut Option<Vec<String>>,
5367 ) -> TextCursor {
5368 if let Some(d) = debug {
5369 d.push(format!(
5370 "[Cursor] move_cursor_down: from byte {} (affinity {:?})",
5371 cursor.cluster_id.start_byte_in_run, cursor.affinity
5372 ));
5373 }
5374
5375 let Some(current_item) = self.items.iter().find(|i| {
5376 i.item
5377 .as_cluster()
5378 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5379 }) else {
5380 if let Some(d) = debug {
5381 d.push(format!(
5382 "[Cursor] move_cursor_down: cursor not found in items, staying at byte {}",
5383 cursor.cluster_id.start_byte_in_run
5384 ));
5385 }
5386 return cursor;
5387 };
5388
5389 if let Some(d) = debug {
5390 d.push(format!(
5391 "[Cursor] move_cursor_down: current line {}, position ({}, {})",
5392 current_item.line_index, current_item.position.x, current_item.position.y
5393 ));
5394 }
5395
5396 let max_line = self.items.iter().map(|i| i.line_index).max().unwrap_or(0);
5397 let target_line_idx = (current_item.line_index + 1).min(max_line);
5398 if current_item.line_index == target_line_idx {
5399 if let Some(d) = debug {
5400 d.push(format!(
5401 "[Cursor] move_cursor_down: already at bottom line {}, staying put",
5402 current_item.line_index
5403 ));
5404 }
5405 return cursor;
5406 }
5407
5408 let current_x = goal_x.unwrap_or_else(|| {
5409 let x = match cursor.affinity {
5410 CursorAffinity::Leading => current_item.position.x,
5411 CursorAffinity::Trailing => {
5412 current_item.position.x + get_item_measure(¤t_item.item, false)
5413 }
5414 };
5415 *goal_x = Some(x);
5416 x
5417 });
5418
5419 let target_y = self
5420 .items
5421 .iter()
5422 .find(|i| i.line_index == target_line_idx)
5423 .map_or(current_item.position.y, |i| i.position.y + (i.item.bounds().height / 2.0));
5424
5425 if let Some(d) = debug {
5426 d.push(format!(
5427 "[Cursor] move_cursor_down: hit testing at ({current_x}, {target_y})"
5428 ));
5429 }
5430
5431 let result = self
5432 .hittest_cursor(LogicalPosition {
5433 x: current_x,
5434 y: target_y,
5435 })
5436 .unwrap_or(cursor);
5437
5438 if let Some(d) = debug {
5439 d.push(format!(
5440 "[Cursor] move_cursor_down: result byte {}, affinity {:?}",
5441 result.cluster_id.start_byte_in_run, result.affinity
5442 ));
5443 }
5444
5445 result
5446 }
5447
5448 pub fn move_cursor_to_line_start(
5450 &self,
5451 cursor: TextCursor,
5452 debug: &mut Option<Vec<String>>,
5453 ) -> TextCursor {
5454 if let Some(d) = debug {
5455 d.push(format!(
5456 "[Cursor] move_cursor_to_line_start: starting at byte {}, affinity {:?}",
5457 cursor.cluster_id.start_byte_in_run, cursor.affinity
5458 ));
5459 }
5460
5461 let Some(current_item) = self.items.iter().find(|i| {
5462 i.item
5463 .as_cluster()
5464 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5465 }) else {
5466 if let Some(d) = debug {
5467 d.push(format!(
5468 "[Cursor] move_cursor_to_line_start: cursor not found, staying at byte {}",
5469 cursor.cluster_id.start_byte_in_run
5470 ));
5471 }
5472 return cursor;
5473 };
5474
5475 if let Some(d) = debug {
5476 d.push(format!(
5477 "[Cursor] move_cursor_to_line_start: current line {}, position ({}, {})",
5478 current_item.line_index, current_item.position.x, current_item.position.y
5479 ));
5480 }
5481
5482 let first_item_on_line = self
5483 .items
5484 .iter()
5485 .filter(|i| i.line_index == current_item.line_index)
5486 .min_by(|a, b| {
5487 a.position
5488 .x
5489 .partial_cmp(&b.position.x)
5490 .unwrap_or(Ordering::Equal)
5491 });
5492
5493 if let Some(item) = first_item_on_line {
5494 if let ShapedItem::Cluster(c) = &item.item {
5495 let result = TextCursor {
5496 cluster_id: c.source_cluster_id,
5497 affinity: CursorAffinity::Leading,
5498 };
5499 if let Some(d) = debug {
5500 d.push(format!(
5501 "[Cursor] move_cursor_to_line_start: result byte {}, affinity {:?}",
5502 result.cluster_id.start_byte_in_run, result.affinity
5503 ));
5504 }
5505 return result;
5506 }
5507 }
5508
5509 if let Some(d) = debug {
5510 d.push(format!(
5511 "[Cursor] move_cursor_to_line_start: no first item found, staying at byte {}",
5512 cursor.cluster_id.start_byte_in_run
5513 ));
5514 }
5515 cursor
5516 }
5517
5518 pub fn move_cursor_to_line_end(
5520 &self,
5521 cursor: TextCursor,
5522 debug: &mut Option<Vec<String>>,
5523 ) -> TextCursor {
5524 if let Some(d) = debug {
5525 d.push(format!(
5526 "[Cursor] move_cursor_to_line_end: starting at byte {}, affinity {:?}",
5527 cursor.cluster_id.start_byte_in_run, cursor.affinity
5528 ));
5529 }
5530
5531 let Some(current_item) = self.items.iter().find(|i| {
5532 i.item
5533 .as_cluster()
5534 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5535 }) else {
5536 if let Some(d) = debug {
5537 d.push(format!(
5538 "[Cursor] move_cursor_to_line_end: cursor not found, staying at byte {}",
5539 cursor.cluster_id.start_byte_in_run
5540 ));
5541 }
5542 return cursor;
5543 };
5544
5545 if let Some(d) = debug {
5546 d.push(format!(
5547 "[Cursor] move_cursor_to_line_end: current line {}, position ({}, {})",
5548 current_item.line_index, current_item.position.x, current_item.position.y
5549 ));
5550 }
5551
5552 let last_item_on_line = self
5553 .items
5554 .iter()
5555 .filter(|i| i.line_index == current_item.line_index)
5556 .max_by(|a, b| {
5557 a.position
5558 .x
5559 .partial_cmp(&b.position.x)
5560 .unwrap_or(Ordering::Equal)
5561 });
5562
5563 if let Some(item) = last_item_on_line {
5564 if let ShapedItem::Cluster(c) = &item.item {
5565 let result = TextCursor {
5566 cluster_id: c.source_cluster_id,
5567 affinity: CursorAffinity::Trailing,
5568 };
5569 if let Some(d) = debug {
5570 d.push(format!(
5571 "[Cursor] move_cursor_to_line_end: result byte {}, affinity {:?}",
5572 result.cluster_id.start_byte_in_run, result.affinity
5573 ));
5574 }
5575 return result;
5576 }
5577 }
5578
5579 if let Some(d) = debug {
5580 d.push(format!(
5581 "[Cursor] move_cursor_to_line_end: no last item found, staying at byte {}",
5582 cursor.cluster_id.start_byte_in_run
5583 ));
5584 }
5585 cursor
5586 }
5587
5588 pub fn move_cursor_to_prev_word(
5596 &self,
5597 cursor: TextCursor,
5598 debug: &mut Option<Vec<String>>,
5599 ) -> TextCursor {
5600 if let Some(d) = debug {
5601 d.push(format!(
5602 "[Cursor] move_cursor_to_prev_word: starting at byte {}, affinity {:?}",
5603 cursor.cluster_id.start_byte_in_run, cursor.affinity
5604 ));
5605 }
5606
5607 let Some(current_pos) = self.items.iter().position(|i| {
5608 i.item
5609 .as_cluster()
5610 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5611 }) else {
5612 return cursor;
5613 };
5614
5615 let mut pos = if cursor.affinity == CursorAffinity::Leading {
5617 current_pos.checked_sub(1)
5619 } else {
5620 Some(current_pos)
5622 };
5623
5624 while let Some(p) = pos {
5626 if let Some(cluster) = self.items[p].item.as_cluster() {
5627 if !cluster_is_word_boundary(cluster) {
5628 break;
5629 }
5630 }
5631 pos = p.checked_sub(1);
5632 }
5633
5634 while let Some(p) = pos {
5636 if let Some(cluster) = self.items[p].item.as_cluster() {
5637 if cluster_is_word_boundary(cluster) {
5638 if p + 1 < self.items.len() {
5640 if let Some(c) = self.items[p + 1].item.as_cluster() {
5641 return TextCursor {
5642 cluster_id: c.source_cluster_id,
5643 affinity: CursorAffinity::Leading,
5644 };
5645 }
5646 }
5647 break;
5648 }
5649 }
5650 if p == 0 {
5651 if let Some(c) = self.items[0].item.as_cluster() {
5653 return TextCursor {
5654 cluster_id: c.source_cluster_id,
5655 affinity: CursorAffinity::Leading,
5656 };
5657 }
5658 break;
5659 }
5660 pos = p.checked_sub(1);
5661 }
5662
5663 if pos.is_none() {
5665 if let Some(first) = self.get_first_cluster_cursor() {
5666 return first;
5667 }
5668 }
5669
5670 cursor
5671 }
5672
5673 pub fn move_cursor_to_next_word(
5680 &self,
5681 cursor: TextCursor,
5682 debug: &mut Option<Vec<String>>,
5683 ) -> TextCursor {
5684 if let Some(d) = debug {
5685 d.push(format!(
5686 "[Cursor] move_cursor_to_next_word: starting at byte {}, affinity {:?}",
5687 cursor.cluster_id.start_byte_in_run, cursor.affinity
5688 ));
5689 }
5690
5691 let Some(current_pos) = self.items.iter().position(|i| {
5692 i.item
5693 .as_cluster()
5694 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5695 }) else {
5696 return cursor;
5697 };
5698
5699 let len = self.items.len();
5700
5701 let start = if cursor.affinity == CursorAffinity::Trailing {
5703 current_pos + 1
5704 } else {
5705 current_pos
5706 };
5707
5708 if start >= len {
5709 return cursor;
5710 }
5711
5712 let mut pos = start;
5713
5714 while pos < len {
5716 if let Some(cluster) = self.items[pos].item.as_cluster() {
5717 if cluster_is_word_boundary(cluster) {
5718 break;
5719 }
5720 }
5721 pos += 1;
5722 }
5723
5724 while pos < len {
5726 if let Some(cluster) = self.items[pos].item.as_cluster() {
5727 if !cluster_is_word_boundary(cluster) {
5728 return TextCursor {
5730 cluster_id: cluster.source_cluster_id,
5731 affinity: CursorAffinity::Leading,
5732 };
5733 }
5734 }
5735 pos += 1;
5736 }
5737
5738 if let Some(last) = self.get_last_cluster_cursor() {
5740 return last;
5741 }
5742
5743 cursor
5744 }
5745}
5746
5747#[allow(clippy::match_same_arms)] fn get_baseline_for_item(item: &ShapedItem) -> Option<f32> {
5749 match item {
5750 ShapedItem::CombinedBlock {
5751 baseline_offset, ..
5752 } => Some(*baseline_offset),
5753 ShapedItem::Object {
5754 baseline_offset, ..
5755 } => Some(*baseline_offset),
5756 ShapedItem::Cluster(ref cluster) => {
5758 cluster.glyphs.last().map(|last_glyph| last_glyph
5759 .font_metrics
5760 .baseline_scaled(last_glyph.style.font_size_px))
5761 }
5762 ShapedItem::Break { source, break_info } => {
5763 None
5765 }
5766 ShapedItem::Tab { source, bounds } => {
5767 None
5769 }
5770 }
5771}
5772
5773#[derive(Debug, Clone, Default)]
5775pub struct OverflowInfo {
5776 pub overflow_items: Vec<ShapedItem>,
5784 pub unclipped_bounds: Rect,
5788}
5789
5790impl OverflowInfo {
5791 #[must_use] pub const fn has_overflow(&self) -> bool {
5792 !self.overflow_items.is_empty()
5793 }
5794}
5795
5796#[derive(Debug, Clone)]
5798pub struct UnifiedLine {
5799 pub items: Vec<ShapedItem>,
5800 pub cross_axis_position: f32,
5802 pub constraints: LineConstraints,
5804 pub is_last: bool,
5805}
5806
5807pub type CacheId = u64;
5810
5811#[derive(Debug, Clone)]
5813pub struct LayoutFragment {
5814 pub id: String,
5816 pub constraints: UnifiedConstraints,
5818}
5819
5820#[derive(Debug, Clone)]
5822pub(crate) struct FlowLayout {
5823 pub(crate) fragment_layouts: HashMap<String, Arc<UnifiedLayout>>,
5825 pub(crate) remaining_items: Vec<ShapedItem>,
5828}
5829
5830#[derive(Copy, Debug, Clone, Default)]
5840pub struct IntrinsicTextSizes {
5841 pub min_content_width: f32,
5843 pub max_content_width: f32,
5845 pub max_content_height: f32,
5847}
5848
5849#[derive(Clone, Debug)]
5855pub struct CachedLineBreaks {
5856 pub line_ranges: Vec<(usize, usize)>,
5858 pub line_widths: Vec<f32>,
5860 pub available_width: f32,
5862}
5863
5864#[derive(Copy, Clone, Debug)]
5866pub enum IncrementalRelayoutResult {
5867 GlyphSwap,
5869 LineShift {
5871 affected_item: usize,
5873 delta: f32,
5875 },
5876 PartialReflow {
5878 reflow_from_line: usize,
5880 },
5881 FullRelayout,
5883}
5884
5885#[must_use] pub fn extract_line_breaks(
5887 items: &[PositionedItem],
5888 available_width: f32,
5889) -> CachedLineBreaks {
5890 let mut line_ranges = Vec::new();
5891 let mut line_widths = Vec::new();
5892
5893 if items.is_empty() {
5894 return CachedLineBreaks { line_ranges, line_widths, available_width };
5895 }
5896
5897 let mut line_start = 0usize;
5898 let mut current_line = items[0].line_index;
5899 let mut line_width = 0.0f32;
5900
5901 for (i, item) in items.iter().enumerate() {
5902 if item.line_index != current_line {
5903 line_ranges.push((line_start, i));
5904 line_widths.push(line_width);
5905 line_start = i;
5906 current_line = item.line_index;
5907 line_width = 0.0;
5908 }
5909 line_width += get_item_measure(&item.item, false);
5910 }
5911
5912 line_ranges.push((line_start, items.len()));
5914 line_widths.push(line_width);
5915
5916 CachedLineBreaks { line_ranges, line_widths, available_width }
5917}
5918
5919#[must_use] pub fn try_incremental_relayout(
5926 dirty_item_indices: &[usize],
5927 old_advances: &[f32],
5928 new_advances: &[f32],
5929 line_breaks: &CachedLineBreaks,
5930) -> IncrementalRelayoutResult {
5931 if dirty_item_indices.is_empty() {
5932 return IncrementalRelayoutResult::GlyphSwap;
5933 }
5934
5935 for &dirty_idx in dirty_item_indices {
5937 if dirty_idx >= old_advances.len() || dirty_idx >= new_advances.len() {
5938 return IncrementalRelayoutResult::FullRelayout;
5939 }
5940
5941 let old_adv = old_advances[dirty_idx];
5942 let new_adv = new_advances[dirty_idx];
5943 let delta = new_adv - old_adv;
5944
5945 if delta.abs() < 0.001 {
5946 continue;
5948 }
5949
5950 let line_idx = line_breaks.line_ranges.iter()
5952 .position(|&(start, end)| dirty_idx >= start && dirty_idx < end);
5953
5954 let Some(line_idx) = line_idx else {
5955 return IncrementalRelayoutResult::FullRelayout;
5956 };
5957
5958 let old_line_width = line_breaks.line_widths[line_idx];
5959 let new_line_width = old_line_width + delta;
5960
5961 if new_line_width <= line_breaks.available_width {
5962 return IncrementalRelayoutResult::LineShift {
5964 affected_item: dirty_idx,
5965 delta,
5966 };
5967 }
5968 return IncrementalRelayoutResult::PartialReflow {
5970 reflow_from_line: line_idx,
5971 };
5972 }
5973
5974 IncrementalRelayoutResult::GlyphSwap
5976}
5977
5978#[derive(Debug)]
5981pub(crate) struct PerItemShapedEntry {
5982 pub(crate) clusters: Vec<ShapedItem>,
5984 pub(crate) total_advance: f32,
5986}
5987
5988#[derive(Debug)]
5989pub struct TextShapingCache {
5990 logical_items: HashMap<CacheId, Arc<Vec<LogicalItem>>>,
5992 visual_items: HashMap<CacheId, Arc<Vec<VisualItem>>>,
5994 shaped_items: HashMap<CacheId, Arc<Vec<ShapedItem>>>,
5996 per_item_shaped: HashMap<u64, Arc<PerItemShapedEntry>>,
5999 per_item_accessed: HashSet<u64>,
6001 generation: u64,
6003}
6004
6005#[derive(Copy, Debug, Clone, Default)]
6007pub struct TextCacheMemoryReport {
6008 pub logical_items_entries: usize,
6009 pub logical_items_bytes: usize,
6010 pub visual_items_entries: usize,
6011 pub visual_items_bytes: usize,
6012 pub shaped_items_entries: usize,
6013 pub shaped_items_bytes: usize,
6014 pub shaped_glyph_bytes: usize,
6015 pub shaped_cluster_text_bytes: usize,
6016 pub per_item_shaped_entries: usize,
6017 pub per_item_shaped_bytes: usize,
6018}
6019
6020impl TextCacheMemoryReport {
6021 #[must_use] pub const fn total_bytes(&self) -> usize {
6022 self.logical_items_bytes
6023 + self.visual_items_bytes
6024 + self.shaped_items_bytes
6025 + self.shaped_glyph_bytes
6026 + self.shaped_cluster_text_bytes
6027 + self.per_item_shaped_bytes
6028 }
6029}
6030
6031impl TextShapingCache {
6032 #[must_use] pub fn new() -> Self {
6033 Self {
6034 logical_items: HashMap::new(),
6035 visual_items: HashMap::new(),
6036 shaped_items: HashMap::new(),
6037 per_item_shaped: HashMap::new(),
6038 per_item_accessed: HashSet::new(),
6039 generation: 0,
6040 }
6041 }
6042
6043 #[allow(clippy::field_reassign_with_default)] #[must_use] pub fn memory_report(&self) -> TextCacheMemoryReport {
6046 let mut r = TextCacheMemoryReport::default();
6047 r.logical_items_entries = self.logical_items.len();
6048 for arc in self.logical_items.values() {
6049 r.logical_items_bytes += arc.capacity() * size_of::<LogicalItem>();
6050 }
6051 r.visual_items_entries = self.visual_items.len();
6052 for arc in self.visual_items.values() {
6053 r.visual_items_bytes += arc.capacity() * size_of::<VisualItem>();
6054 }
6055 r.shaped_items_entries = self.shaped_items.len();
6056 for arc in self.shaped_items.values() {
6057 r.shaped_items_bytes += arc.capacity() * size_of::<ShapedItem>();
6058 for item in arc.iter() {
6059 if let ShapedItem::Cluster(c) = item {
6060 r.shaped_glyph_bytes += c.glyphs.capacity() * size_of::<ShapedGlyph>();
6061 r.shaped_cluster_text_bytes += c.text.capacity();
6062 }
6063 }
6064 }
6065 r.per_item_shaped_entries = self.per_item_shaped.len();
6066 for arc in self.per_item_shaped.values() {
6067 r.per_item_shaped_bytes += arc.clusters.capacity() * size_of::<ShapedItem>();
6068 for item in &arc.clusters {
6069 if let ShapedItem::Cluster(c) = item {
6070 r.per_item_shaped_bytes += c.glyphs.capacity() * size_of::<ShapedGlyph>();
6071 r.per_item_shaped_bytes += c.text.capacity();
6072 }
6073 }
6074 }
6075 r
6076 }
6077
6078 pub fn begin_generation(&mut self) {
6081 if self.generation > 0 && !self.per_item_accessed.is_empty() {
6082 let accessed = &self.per_item_accessed;
6084 self.per_item_shaped.retain(|k, _| accessed.contains(k));
6085 }
6086 self.per_item_accessed.clear();
6087 self.generation += 1;
6088 }
6089
6090 #[must_use] pub fn use_old_layout(
6105 old_constraints: &UnifiedConstraints,
6106 new_constraints: &UnifiedConstraints,
6107 old_content: &[InlineContent],
6108 new_content: &[InlineContent],
6109 ) -> bool {
6110 if old_constraints != new_constraints {
6112 return false;
6113 }
6114
6115 if old_content.len() != new_content.len() {
6117 return false;
6118 }
6119
6120 for (old, new) in old_content.iter().zip(new_content.iter()) {
6122 if !Self::inline_content_layout_eq(old, new) {
6123 return false;
6124 }
6125 }
6126
6127 true
6128 }
6129
6130 fn inline_content_layout_eq(old: &InlineContent, new: &InlineContent) -> bool {
6134 use InlineContent::{Text, Image, Space, LineBreak, Tab, Marker, Shape, Ruby};
6135 match (old, new) {
6136 (Text(old_run), Text(new_run)) => {
6137 old_run.text == new_run.text
6139 && old_run.style.layout_eq(&new_run.style)
6140 }
6141 (Image(old_img), Image(new_img)) => {
6142 old_img.intrinsic_size == new_img.intrinsic_size
6144 && old_img.display_size == new_img.display_size
6145 && old_img.baseline_offset == new_img.baseline_offset
6146 && old_img.alignment == new_img.alignment
6147 }
6148 (Space(old_sp), Space(new_sp)) => old_sp == new_sp,
6149 (LineBreak(old_br), LineBreak(new_br)) => old_br == new_br,
6150 (Tab { style: old_style }, Tab { style: new_style }) => old_style.layout_eq(new_style),
6151 (Marker { run: old_run, position_outside: old_pos },
6152 Marker { run: new_run, position_outside: new_pos }) => {
6153 old_pos == new_pos
6154 && old_run.text == new_run.text
6155 && old_run.style.layout_eq(&new_run.style)
6156 }
6157 (Shape(old_shape), Shape(new_shape)) => {
6158 old_shape.shape_def == new_shape.shape_def
6160 && old_shape.baseline_offset == new_shape.baseline_offset
6161 }
6162 (Ruby { base: old_base, text: old_text, style: old_style },
6163 Ruby { base: new_base, text: new_text, style: new_style }) => {
6164 old_style.layout_eq(new_style)
6165 && old_base.len() == new_base.len()
6166 && old_text.len() == new_text.len()
6167 && old_base.iter().zip(new_base.iter())
6168 .all(|(o, n)| Self::inline_content_layout_eq(o, n))
6169 && old_text.iter().zip(new_text.iter())
6170 .all(|(o, n)| Self::inline_content_layout_eq(o, n))
6171 }
6172 _ => false,
6174 }
6175 }
6176}
6177
6178impl Default for TextShapingCache {
6179 fn default() -> Self {
6180 Self::new()
6181 }
6182}
6183
6184#[derive(Debug, Clone, Eq, PartialEq, Hash)]
6186pub(crate) struct LogicalItemsKey<'a> {
6187 pub(crate) inline_content_hash: u64,
6188 pub(crate) default_font_size: u32,
6189 pub(crate) _marker: std::marker::PhantomData<&'a ()>,
6190}
6191
6192#[derive(Debug, Clone, Eq, PartialEq, Hash)]
6194pub(crate) struct VisualItemsKey {
6195 pub(crate) logical_items_id: CacheId,
6196 pub(crate) base_direction: BidiDirection,
6197}
6198
6199#[derive(Debug, Clone, Eq, PartialEq, Hash)]
6201pub(crate) struct ShapedItemsKey {
6202 pub(crate) visual_items_id: CacheId,
6203 pub(crate) style_hash: u64,
6204}
6205
6206impl ShapedItemsKey {
6207 pub(crate) fn new(visual_items_id: CacheId, visual_items: &[VisualItem]) -> Self {
6208 let style_hash = {
6209 let mut hasher = DefaultHasher::new();
6210 for item in visual_items {
6211 match &item.logical_source {
6213 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
6214 style.as_ref().hash(&mut hasher);
6215 }
6216 _ => {}
6217 }
6218 }
6219 hasher.finish()
6220 };
6221
6222 Self {
6223 visual_items_id,
6224 style_hash,
6225 }
6226 }
6227}
6228
6229#[derive(Debug, Clone, Eq, PartialEq, Hash)]
6231pub(crate) struct LayoutKey {
6232 pub(crate) shaped_items_id: CacheId,
6233 pub(crate) constraints: UnifiedConstraints,
6234}
6235
6236fn calculate_id<T: Hash>(item: &T) -> CacheId {
6238 let mut hasher = DefaultHasher::new();
6239 item.hash(&mut hasher);
6240 hasher.finish()
6241}
6242
6243impl TextShapingCache {
6246 #[allow(clippy::too_many_lines)] pub fn layout_flow<T: ParsedFontTrait>(
6311 &mut self,
6312 content: &[InlineContent],
6313 style_overrides: &[StyleOverride],
6314 flow_chain: &[LayoutFragment],
6315 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
6316 fc_cache: &FcFontCache,
6317 loaded_fonts: &LoadedFonts<T>,
6318 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6319 ) -> Result<FlowLayout, LayoutError> {
6320 #[cfg(feature = "web_lift")]
6322 unsafe {
6323 crate::az_mark((0x60BD0) as u32, (content.as_ptr() as usize as u32) as u32);
6324 crate::az_mark((0x60BD4) as u32, (content.len() as u32 | 0xC0DE0000) as u32);
6325 }
6326 const PER_ITEM_CACHE_MAX: usize = 4096;
6339 if self.per_item_shaped.len() > PER_ITEM_CACHE_MAX {
6340 self.begin_generation();
6341 }
6342
6343 let logical_items_id = calculate_id(&content);
6353 let logical_items = self
6354 .logical_items
6355 .entry(logical_items_id)
6356 .or_insert_with(|| {
6357 Arc::new(create_logical_items(content, style_overrides, debug_messages))
6358 })
6359 .clone();
6360
6361 let default_constraints = UnifiedConstraints::default();
6364 let first_constraints = flow_chain
6365 .first()
6366 .map_or(&default_constraints, |f| &f.constraints);
6367
6368 let unicode_bidi_val = first_constraints.unicode_bidi;
6380 let base_direction = if unicode_bidi_val == UnicodeBidi::Plaintext {
6381 let has_strong = logical_items.iter().any(|item| {
6383 if let LogicalItem::Text { text, .. } = item {
6384 matches!(unicode_bidi::get_base_direction(text.as_str()),
6385 unicode_bidi::Direction::Ltr | unicode_bidi::Direction::Rtl)
6386 } else {
6387 false
6388 }
6389 });
6390 if has_strong {
6391 get_base_direction_from_logical(&logical_items)
6392 } else {
6393 first_constraints.direction.unwrap_or(BidiDirection::Ltr)
6395 }
6396 } else {
6397 first_constraints.direction.unwrap_or(BidiDirection::Ltr)
6399 };
6400 let visual_key = VisualItemsKey {
6401 logical_items_id,
6402 base_direction,
6403 };
6404 let visual_items_id = calculate_id(&visual_key);
6405 let visual_items = self
6408 .visual_items
6409 .entry(visual_items_id)
6410 .or_insert_with(|| {
6411 Arc::new(
6412 reorder_logical_items(&logical_items, base_direction, unicode_bidi_val, debug_messages).unwrap(),
6413 )
6414 })
6415 .clone();
6416
6417 let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
6420 let shaped_items_id = calculate_id(&shaped_key);
6421 let shaped_items = if let Some(cached) = self.shaped_items.get(&shaped_items_id) {
6423 cached.clone()
6425 } else {
6426 let items = Arc::new(shape_visual_items_with_per_item_cache(
6429 &visual_items,
6430 &mut self.per_item_shaped,
6431 &mut self.per_item_accessed,
6432 font_chain_cache,
6433 fc_cache,
6434 loaded_fonts,
6435 debug_messages,
6436 )?);
6437 self.shaped_items.insert(shaped_items_id, items.clone());
6438 items
6439 };
6440
6441 let oriented_items = apply_text_orientation(shaped_items, first_constraints);
6448
6449 let mut fragment_layouts = HashMap::new();
6451 let mut cursor = BreakCursor::with_word_break(&oriented_items, first_constraints.word_break);
6454 cursor.hyphens = first_constraints.hyphenation;
6455 cursor.line_break = first_constraints.line_break;
6456
6457 #[allow(clippy::no_effect_underscore_binding)] let mut _az_flow_iters: usize = 0;
6464 for fragment in flow_chain {
6465 #[cfg(feature = "web_lift")]
6466 {
6467 _az_flow_iters += 1;
6468 unsafe { crate::az_mark((0x60BC0) as u32, (_az_flow_iters as u32 | 0xC0DE0000) as u32); }
6469 if _az_flow_iters > 256 {
6470 break;
6471 }
6472 }
6473 let fragment_layout = perform_fragment_layout(
6475 &mut cursor,
6476 &logical_items,
6477 &fragment.constraints,
6478 debug_messages,
6479 loaded_fonts,
6480 )?;
6481
6482 fragment_layouts.insert(fragment.id.clone(), Arc::new(fragment_layout));
6483 if cursor.is_done() {
6484 break; }
6486 }
6487
6488 Ok(FlowLayout {
6489 fragment_layouts,
6490 remaining_items: cursor.drain_remaining(),
6491 })
6492 }
6493
6494 #[allow(clippy::too_many_lines)] pub fn measure_intrinsic_widths<T: ParsedFontTrait>(
6517 &mut self,
6518 content: &[InlineContent],
6519 style_overrides: &[StyleOverride],
6520 constraints: &UnifiedConstraints,
6521 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
6522 fc_cache: &FcFontCache,
6523 loaded_fonts: &LoadedFonts<T>,
6524 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6525 ) -> Result<IntrinsicTextSizes, LayoutError> {
6526 const PER_ITEM_CACHE_MAX: usize = 4096;
6527 if self.per_item_shaped.len() > PER_ITEM_CACHE_MAX {
6528 self.begin_generation();
6529 }
6530
6531 let logical_items_id = calculate_id(&content);
6535 let logical_items = self
6536 .logical_items
6537 .entry(logical_items_id)
6538 .or_insert_with(|| {
6539 Arc::new(create_logical_items(content, style_overrides, debug_messages))
6540 })
6541 .clone();
6542
6543 let unicode_bidi_val = constraints.unicode_bidi;
6545 let base_direction = if unicode_bidi_val == UnicodeBidi::Plaintext {
6546 let has_strong = logical_items.iter().any(|item| {
6547 if let LogicalItem::Text { text, .. } = item {
6548 matches!(unicode_bidi::get_base_direction(text.as_str()),
6549 unicode_bidi::Direction::Ltr | unicode_bidi::Direction::Rtl)
6550 } else {
6551 false
6552 }
6553 });
6554 if has_strong {
6555 get_base_direction_from_logical(&logical_items)
6556 } else {
6557 constraints.direction.unwrap_or(BidiDirection::Ltr)
6558 }
6559 } else {
6560 constraints.direction.unwrap_or(BidiDirection::Ltr)
6561 };
6562 let visual_key = VisualItemsKey {
6563 logical_items_id,
6564 base_direction,
6565 };
6566 let visual_items_id = calculate_id(&visual_key);
6567 let visual_items = self
6568 .visual_items
6569 .entry(visual_items_id)
6570 .or_insert_with(|| {
6571 Arc::new(
6572 reorder_logical_items(&logical_items, base_direction, unicode_bidi_val, debug_messages).unwrap(),
6573 )
6574 })
6575 .clone();
6576
6577 let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
6579 let shaped_items_id = calculate_id(&shaped_key);
6580 let shaped_items = if let Some(cached) = self.shaped_items.get(&shaped_items_id) { cached.clone() } else {
6581 let items = Arc::new(shape_visual_items_with_per_item_cache(
6582 &visual_items,
6583 &mut self.per_item_shaped,
6584 &mut self.per_item_accessed,
6585 font_chain_cache,
6586 fc_cache,
6587 loaded_fonts,
6588 debug_messages,
6589 )?);
6590 self.shaped_items.insert(shaped_items_id, items.clone());
6591 items
6592 };
6593
6594 let oriented_items = apply_text_orientation(shaped_items, constraints);
6596
6597 let word_break = constraints.word_break;
6599 let hyphens = constraints.hyphenation;
6600
6601 let mut total = 0.0f32; let mut max_line = 0.0f32; let mut max_word = 0.0f32;
6604 let mut cur_word = 0.0f32;
6605 let mut max_line_height = 0.0f32;
6606
6607 for item in oriented_items.iter() {
6608 if let ShapedItem::Break { .. } = item {
6614 if total > max_line { max_line = total; }
6615 if cur_word > max_word { max_word = cur_word; }
6616 total = 0.0;
6617 cur_word = 0.0;
6618 continue;
6619 }
6620 let advance = match item {
6629 ShapedItem::Cluster(c) => {
6630 let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
6631 let mut a = c.advance + total_kerning;
6632 if !is_cursive_script_cluster(c) {
6637 a += c.style.letter_spacing.resolve_px(c.style.font_size_px);
6638 }
6639 if is_word_separator(item) {
6640 a += c.style.word_spacing.resolve_px(c.style.font_size_px);
6641 }
6642 a
6643 }
6644 ShapedItem::CombinedBlock { bounds, .. }
6645 | ShapedItem::Object { bounds, .. }
6646 | ShapedItem::Tab { bounds, .. } => bounds.width,
6647 ShapedItem::Break { .. } => 0.0,
6648 };
6649 let adv = advance.max(0.0);
6650 total += adv;
6651
6652 let (asc, desc) = get_item_vertical_metrics_approx(item);
6653 let h = (asc + desc).max(item.bounds().height);
6654 if h > max_line_height {
6655 max_line_height = h;
6656 }
6657
6658 if is_break_opportunity_with_word_break(item, word_break, hyphens) {
6659 if cur_word > max_word {
6660 max_word = cur_word;
6661 }
6662 if !is_word_separator(item) && adv > max_word {
6669 max_word = adv;
6670 }
6671 cur_word = 0.0;
6672 } else {
6673 cur_word += adv;
6674 }
6675 }
6676 if cur_word > max_word {
6677 max_word = cur_word;
6678 }
6679 if total > max_line {
6680 max_line = total;
6681 }
6682
6683 let min_content_width = if matches!(constraints.white_space_mode, WhiteSpaceMode::Nowrap) {
6688 max_line
6689 } else {
6690 max_word
6691 };
6692
6693 Ok(IntrinsicTextSizes {
6694 min_content_width,
6695 max_content_width: max_line,
6696 max_content_height: max_line_height,
6697 })
6698 }
6699}
6700
6701#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn create_logical_items(
6708 content: &[InlineContent],
6709 style_overrides: &[StyleOverride],
6710 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6711) -> Vec<LogicalItem> {
6712 if let Some(msgs) = debug_messages {
6713 msgs.push(LayoutDebugMessage::info(
6714 "\n--- Entering create_logical_items (Refactored) ---".to_string(),
6715 ));
6716 msgs.push(LayoutDebugMessage::info(format!(
6717 "Input content length: {}",
6718 content.len()
6719 )));
6720 msgs.push(LayoutDebugMessage::info(format!(
6721 "Input overrides length: {}",
6722 style_overrides.len()
6723 )));
6724 }
6725
6726 let mut items: Vec<LogicalItem> = Vec::new();
6727 let mut style_cache: HashMap<u64, Arc<StyleProperties>> = HashMap::new();
6728
6729 let mut run_overrides: HashMap<u32, HashMap<u32, &PartialStyleProperties>> = HashMap::new();
6731 for override_item in style_overrides {
6732 run_overrides
6733 .entry(override_item.target.run_index)
6734 .or_default()
6735 .insert(override_item.target.item_index, &override_item.style);
6736 }
6737
6738 for (run_idx, inline_item) in content.iter().enumerate() {
6739 if let Some(msgs) = debug_messages {
6740 msgs.push(LayoutDebugMessage::info(format!(
6741 "Processing content run #{run_idx}"
6742 )));
6743 }
6744
6745 let marker_position_outside = match inline_item {
6747 InlineContent::Marker {
6748 position_outside, ..
6749 } => Some(*position_outside),
6750 _ => None,
6751 };
6752
6753 if let InlineContent::Text(run) | InlineContent::Marker { run, .. } = inline_item {
6761 let text = &run.text;
6762 if text.is_empty() {
6763 if let Some(msgs) = debug_messages {
6764 msgs.push(LayoutDebugMessage::info(
6765 " Run is empty, skipping.".to_string(),
6766 ));
6767 }
6768 continue;
6769 }
6770 if let Some(msgs) = debug_messages {
6771 msgs.push(LayoutDebugMessage::info(format!(" Run text: '{text}'")));
6772 }
6773
6774 let current_run_overrides = run_overrides.get(&(run_idx as u32));
6775 let mut boundaries = BTreeSet::new();
6776 boundaries.insert(0);
6777 boundaries.insert(text.len());
6778
6779 let needs_scan = current_run_overrides.is_some()
6787 || run.style.text_combine_upright.is_some();
6788 let mut scan_cursor = 0;
6789 while needs_scan && scan_cursor < text.len() {
6790 let style_at_cursor = current_run_overrides.and_then(|o| o.get(&(scan_cursor as u32))).map_or_else(|| (*run.style).clone(), |partial| run.style.apply_override(partial));
6791
6792 let current_char = text[scan_cursor..].chars().next().unwrap();
6793
6794 if let Some(TextCombineUpright::Digits(max_digits)) =
6799 style_at_cursor.text_combine_upright
6800 {
6801 if max_digits > 0 && current_char.is_ascii_digit() {
6802 let digit_chunk: String = text[scan_cursor..]
6803 .chars()
6804 .take(max_digits as usize)
6805 .take_while(char::is_ascii_digit)
6806 .collect();
6807
6808 let end_of_chunk = scan_cursor + digit_chunk.len();
6809 boundaries.insert(scan_cursor);
6810 boundaries.insert(end_of_chunk);
6811 scan_cursor = end_of_chunk; continue;
6813 }
6814 }
6815
6816 if current_run_overrides
6819 .and_then(|o| o.get(&(scan_cursor as u32)))
6820 .is_some()
6821 {
6822 let grapheme_len = text[scan_cursor..]
6823 .graphemes(true)
6824 .next()
6825 .unwrap_or("")
6826 .len();
6827 boundaries.insert(scan_cursor);
6828 boundaries.insert(scan_cursor + grapheme_len);
6829 scan_cursor += grapheme_len;
6830 continue;
6831 }
6832
6833 scan_cursor += current_char.len_utf8();
6836 }
6837
6838 if let Some(msgs) = debug_messages {
6839 msgs.push(LayoutDebugMessage::info(format!(
6840 " Boundaries: {boundaries:?}"
6841 )));
6842 }
6843
6844 for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
6846 let (start, end) = (*start, *end);
6847 if start >= end {
6848 continue;
6849 }
6850
6851 let text_slice = &text[start..end];
6852 if let Some(msgs) = debug_messages {
6853 msgs.push(LayoutDebugMessage::info(format!(
6854 " Processing chunk from {start} to {end}: '{text_slice}'"
6855 )));
6856 }
6857
6858 let style_to_use = current_run_overrides.and_then(|o| o.get(&(start as u32))).map_or_else(|| run.style.clone(), |partial_style| {
6859 if let Some(msgs) = debug_messages {
6860 msgs.push(LayoutDebugMessage::info(format!(
6861 " -> Applying override at byte {start}"
6862 )));
6863 }
6864 let mut hasher = DefaultHasher::new();
6865 Arc::as_ptr(&run.style).hash(&mut hasher);
6866 partial_style.hash(&mut hasher);
6867 style_cache
6868 .entry(hasher.finish())
6869 .or_insert_with(|| Arc::new(run.style.apply_override(partial_style)))
6870 .clone()
6871 });
6872
6873 let is_combinable_chunk = match &style_to_use.text_combine_upright {
6880 Some(TextCombineUpright::All) => !text_slice.is_empty(),
6881 Some(TextCombineUpright::Digits(max_digits)) => {
6882 *max_digits > 0
6883 && !text_slice.is_empty()
6884 && text_slice.chars().all(|c| c.is_ascii_digit())
6885 && text_slice.chars().count() <= *max_digits as usize
6886 }
6887 _ => false,
6888 };
6889
6890 if is_combinable_chunk {
6891 let trimmed = text_slice.trim();
6893 let combined_text = if trimmed.is_empty() {
6894 text_slice.to_string()
6895 } else {
6896 trimmed.to_string()
6897 };
6898 items.push(LogicalItem::CombinedText {
6899 source: ContentIndex {
6900 run_index: run_idx as u32,
6901 item_index: start as u32,
6902 },
6903 text: combined_text,
6904 style: style_to_use,
6905 });
6906 } else {
6907 items.push(LogicalItem::Text {
6908 source: ContentIndex {
6909 run_index: run_idx as u32,
6910 item_index: start as u32,
6911 },
6912 text: text_slice.to_string(),
6913 style: style_to_use,
6914 marker_position_outside,
6915 source_node_id: run.source_node_id,
6916 });
6917 }
6918 }
6919 } else {
6920 match inline_item {
6921 InlineContent::LineBreak(break_info) => {
6923 if let Some(msgs) = debug_messages {
6924 msgs.push(LayoutDebugMessage::info(format!(
6925 " LineBreak: {break_info:?}"
6926 )));
6927 }
6928 items.push(LogicalItem::Break {
6929 source: ContentIndex {
6930 run_index: run_idx as u32,
6931 item_index: 0,
6932 },
6933 break_info: *break_info,
6934 });
6935 }
6936 InlineContent::Tab { style } => {
6938 if let Some(msgs) = debug_messages {
6939 msgs.push(LayoutDebugMessage::info(" Tab character".to_string()));
6940 }
6941 items.push(LogicalItem::Tab {
6942 source: ContentIndex {
6943 run_index: run_idx as u32,
6944 item_index: 0,
6945 },
6946 style: style.clone(),
6947 });
6948 }
6949 _ => {
6952 if let Some(msgs) = debug_messages {
6953 msgs.push(LayoutDebugMessage::info(
6954 " Run is not text, creating generic LogicalItem.".to_string(),
6955 ));
6956 }
6957 items.push(LogicalItem::Object {
6958 source: ContentIndex {
6959 run_index: run_idx as u32,
6960 item_index: 0,
6961 },
6962 content: inline_item.clone(),
6963 });
6964 }
6965 }
6966 }
6967 }
6968 if let Some(msgs) = debug_messages {
6969 msgs.push(LayoutDebugMessage::info(format!(
6970 "--- Exiting create_logical_items, created {} items ---",
6971 items.len()
6972 )));
6973 }
6974 items
6975}
6976
6977#[must_use] pub fn get_base_direction_from_logical(logical_items: &[LogicalItem]) -> BidiDirection {
6983 let first_strong = logical_items.iter().find_map(|item| {
6984 if let LogicalItem::Text { text, .. } = item {
6985 Some(unicode_bidi::get_base_direction(text.as_str()))
6986 } else {
6987 None
6988 }
6989 });
6990
6991 match first_strong {
6992 Some(unicode_bidi::Direction::Rtl) => BidiDirection::Rtl,
6993 _ => BidiDirection::Ltr,
6994 }
6995}
6996
6997#[allow(clippy::match_same_arms)] #[allow(clippy::too_many_lines)] pub fn reorder_logical_items(
7014 logical_items: &[LogicalItem],
7015 base_direction: BidiDirection,
7016 unicode_bidi: UnicodeBidi,
7017 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
7018) -> Result<Vec<VisualItem>, LayoutError> {
7019 if let Some(msgs) = debug_messages {
7020 msgs.push(LayoutDebugMessage::info(
7021 "\n--- Entering reorder_logical_items ---".to_string(),
7022 ));
7023 msgs.push(LayoutDebugMessage::info(format!(
7024 "Input logical items count: {}",
7025 logical_items.len()
7026 )));
7027 msgs.push(LayoutDebugMessage::info(format!(
7028 "Base direction: {base_direction:?}"
7029 )));
7030 }
7031
7032 let mut bidi_str = String::new();
7034 let mut item_map = Vec::new();
7035 let mut logical_item_starts = Vec::with_capacity(logical_items.len());
7039 for (idx, item) in logical_items.iter().enumerate() {
7040 let text = match item {
7059 LogicalItem::Text { text, .. } => text.as_str(),
7060 LogicalItem::CombinedText { text, .. } => text.as_str(),
7061 _ => "\u{FFFC}",
7062 };
7063 let start_byte = bidi_str.len();
7064 logical_item_starts.push(start_byte);
7065 bidi_str.push_str(text);
7066 for _ in start_byte..bidi_str.len() {
7067 item_map.push(idx);
7068 }
7069 }
7070
7071 if bidi_str.is_empty() {
7072 if let Some(msgs) = debug_messages {
7073 msgs.push(LayoutDebugMessage::info(
7074 "Bidi string is empty, returning.".to_string(),
7075 ));
7076 }
7077 return Ok(Vec::new());
7078 }
7079 if let Some(msgs) = debug_messages {
7080 msgs.push(LayoutDebugMessage::info(format!(
7081 "Constructed bidi string: '{bidi_str}'"
7082 )));
7083 }
7084
7085 let bidi_level = if unicode_bidi == UnicodeBidi::Plaintext {
7090 None
7091 } else if base_direction == BidiDirection::Rtl {
7092 Some(Level::rtl())
7093 } else {
7094 Some(Level::ltr())
7095 };
7096 let bidi_info = BidiInfo::new(&bidi_str, bidi_level);
7098 let para = &bidi_info.paragraphs[0];
7099 let (levels, visual_runs) = bidi_info.visual_runs(para, para.range.clone());
7100
7101 if let Some(msgs) = debug_messages {
7102 msgs.push(LayoutDebugMessage::info(
7103 "Bidi visual runs generated:".to_string(),
7104 ));
7105 for (i, run_range) in visual_runs.iter().enumerate() {
7106 let level = levels[run_range.start].number();
7107 let slice = &bidi_str[run_range.start..run_range.end];
7108 msgs.push(LayoutDebugMessage::info(format!(
7109 " Run {i}: range={run_range:?}, level={level}, text='{slice}'"
7110 )));
7111 }
7112 }
7113
7114 let mut visual_items = Vec::new();
7130 for run_range in visual_runs {
7131 let bidi_level = BidiLevel::new(levels[run_range.start].number());
7132 let mut sub_run_start = run_range.start;
7133
7134 for i in (run_range.start + 1)..run_range.end {
7135 if item_map[i] != item_map[sub_run_start] {
7136 let logical_idx = item_map[sub_run_start];
7137 let logical_item = &logical_items[logical_idx];
7138 let text_slice = &bidi_str[sub_run_start..i];
7139 visual_items.push(VisualItem {
7140 logical_source: logical_item.clone(),
7141 bidi_level,
7142 script: crate::text3::script::detect_script(text_slice)
7143 .unwrap_or(Script::Latin),
7144 text: text_slice.to_string(),
7145 run_byte_offset: sub_run_start - logical_item_starts[logical_idx],
7146 });
7147 sub_run_start = i;
7148 }
7149 }
7150
7151 let logical_idx = item_map[sub_run_start];
7152 let logical_item = &logical_items[logical_idx];
7153 let text_slice = &bidi_str[sub_run_start..run_range.end];
7154 visual_items.push(VisualItem {
7155 logical_source: logical_item.clone(),
7156 bidi_level,
7157 script: crate::text3::script::detect_script(text_slice).unwrap_or(Script::Latin),
7158 text: text_slice.to_string(),
7159 run_byte_offset: sub_run_start - logical_item_starts[logical_idx],
7160 });
7161 }
7162
7163 if let Some(msgs) = debug_messages {
7164 msgs.push(LayoutDebugMessage::info(
7165 "Final visual items produced:".to_string(),
7166 ));
7167 for (i, item) in visual_items.iter().enumerate() {
7168 msgs.push(LayoutDebugMessage::info(format!(
7169 " Item {}: level={}, text='{}'",
7170 i,
7171 item.bidi_level.level(),
7172 item.text
7173 )));
7174 }
7175 msgs.push(LayoutDebugMessage::info(
7176 "--- Exiting reorder_logical_items ---".to_string(),
7177 ));
7178 }
7179 Ok(visual_items)
7180}
7181
7182#[allow(clippy::implicit_hasher)] pub fn shape_visual_items_with_per_item_cache<T: ParsedFontTrait>(
7211 visual_items: &[VisualItem],
7212 per_item_cache: &mut HashMap<u64, Arc<PerItemShapedEntry>>,
7213 per_item_accessed: &mut HashSet<u64>,
7214 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
7215 fc_cache: &FcFontCache,
7216 loaded_fonts: &LoadedFonts<T>,
7217 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
7218) -> Result<Vec<ShapedItem>, LayoutError> {
7219 use std::hash::{Hash, Hasher};
7220 let mut shaped = Vec::new();
7227 let mut idx = 0;
7228
7229 while idx < visual_items.len() {
7230 let item = &visual_items[idx];
7231
7232 let (layout_hash, bidi_level, script) = match &item.logical_source {
7234 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
7235 (style.layout_hash(), item.bidi_level, item.script)
7236 }
7237 _ => {
7238 let single = shape_visual_items(
7240 &visual_items[idx..=idx],
7241 font_chain_cache, fc_cache, loaded_fonts, debug_messages,
7242 )?;
7243 shaped.extend(single);
7244 idx += 1;
7245 continue;
7246 }
7247 };
7248
7249 let mut coalesce_end = idx + 1;
7250 while coalesce_end < visual_items.len() {
7251 let next = &visual_items[coalesce_end];
7252 let next_layout_hash = match &next.logical_source {
7253 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
7254 Some(style.layout_hash())
7255 }
7256 _ => None,
7257 };
7258 if let Some(nlh) = next_layout_hash {
7259 if nlh == layout_hash
7260 && next.bidi_level == bidi_level
7261 && next.script == script
7262 {
7263 coalesce_end += 1;
7264 } else {
7265 break;
7266 }
7267 } else {
7268 break;
7269 }
7270 }
7271
7272 let mut hasher = DefaultHasher::new();
7274 for item in &visual_items[idx..coalesce_end] {
7275 item.text.hash(&mut hasher);
7276 }
7277 layout_hash.hash(&mut hasher);
7278 bidi_level.hash(&mut hasher);
7279 (script as u32).hash(&mut hasher);
7280 let group_key = hasher.finish();
7281
7282 per_item_accessed.insert(group_key);
7284 if let Some(cached) = per_item_cache.get(&group_key) {
7285 shaped.extend(cached.clusters.iter().cloned());
7286 } else {
7287 let group_items = shape_visual_items(
7289 &visual_items[idx..coalesce_end],
7290 font_chain_cache, fc_cache, loaded_fonts, debug_messages,
7291 )?;
7292 let total_advance: f32 = group_items.iter().map(|item| {
7293 match item {
7294 ShapedItem::Cluster(c) => c.advance,
7295 _ => 0.0,
7296 }
7297 }).sum();
7298 per_item_cache.insert(group_key, Arc::new(PerItemShapedEntry {
7299 clusters: group_items.clone(),
7300 total_advance,
7301 }));
7302 shaped.extend(group_items);
7303 }
7304
7305 idx = coalesce_end;
7306 }
7307
7308 Ok(shaped)
7309}
7310
7311fn split_text_by_font_coverage<T: ParsedFontTrait>(
7316 text: &str,
7317 font_chain: &rust_fontconfig::FontFallbackChain,
7318 fc_cache: &FcFontCache,
7319 loaded_fonts: &LoadedFonts<T>,
7320) -> Vec<(usize, usize, FontId)> {
7321 let mut segments: Vec<(usize, usize, FontId)> = Vec::new();
7322
7323 let notdef_font_id = loaded_fonts.iter().map(|(id, _)| *id).min();
7327
7328 for (byte_idx, ch) in text.char_indices() {
7329 let char_end = byte_idx + ch.len_utf8();
7330 let font_id = font_chain
7336 .resolve_char(fc_cache, ch)
7337 .map(|(id, _)| id)
7338 .or_else(|| {
7346 loaded_fonts
7347 .iter()
7348 .filter(|(_, font)| font.has_glyph(ch as u32))
7349 .map(|(id, _)| *id)
7350 .min()
7351 })
7352 .or(notdef_font_id);
7356 if let Some(font_id) = font_id {
7357 match segments.last_mut() {
7358 Some(last) if last.2 == font_id && last.1 == byte_idx => {
7359 last.1 = char_end;
7361 }
7362 _ => {
7363 segments.push((byte_idx, char_end, font_id));
7365 }
7366 }
7367 }
7368 }
7369
7370 segments
7371}
7372
7373fn measure_run_advance<T: ParsedFontTrait>(
7380 text: &str,
7381 style: &Arc<StyleProperties>,
7382 script: Script,
7383 source: ContentIndex,
7384 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
7385 fc_cache: &FcFontCache,
7386 loaded_fonts: &LoadedFonts<T>,
7387) -> Option<f32> {
7388 if text.is_empty() {
7389 return Some(0.0);
7390 }
7391 let language = script_to_language(script, text);
7392 match &style.font_stack {
7393 FontStack::Ref(font_ref) => {
7394 let glyphs = font_ref
7395 .shape_text(text, script, language, BidiDirection::Ltr, style.as_ref())
7396 .ok()?;
7397 Some(glyphs.iter().map(|g| g.advance + g.kerning).sum())
7398 }
7399 FontStack::Stack(selectors) => {
7400 let cache_key = FontChainKey::from_selectors(selectors);
7401 let font_chain = font_chain_cache.get(&cache_key)?;
7402 let clusters = shape_with_font_fallback(
7403 text, script, language, BidiDirection::Ltr, style, source, None, font_chain,
7404 fc_cache, loaded_fonts,
7405 )
7406 .ok()?;
7407 Some(clusters.iter().map(|c| c.advance).sum())
7408 }
7409 }
7410}
7411
7412#[allow(clippy::cast_possible_truncation)] fn shape_with_font_fallback<T: ParsedFontTrait>(
7419 text: &str,
7420 script: Script,
7421 language: Language,
7422 direction: BidiDirection,
7423 style: &Arc<StyleProperties>,
7424 source_index: ContentIndex,
7425 source_node_id: Option<NodeId>,
7426 font_chain: &rust_fontconfig::FontFallbackChain,
7427 fc_cache: &FcFontCache,
7428 loaded_fonts: &LoadedFonts<T>,
7429) -> Result<Vec<ShapedCluster>, LayoutError> {
7430 static FONT_FB_DEBUG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7438 let dbg = *FONT_FB_DEBUG.get_or_init(|| {
7439 std::env::var_os("AZ_FONT_FALLBACK_DEBUG").is_some()
7440 });
7441
7442 let segments = split_text_by_font_coverage(text, font_chain, fc_cache, loaded_fonts);
7443
7444 if dbg && segments.len() > 1 {
7445 eprintln!(
7446 "[FONT FALLBACK] text needs {} font segments for '{}' ({}..{} bytes)",
7447 segments.len(),
7448 text.chars().take(40).collect::<String>(),
7449 0, text.len()
7450 );
7451 }
7452
7453 unsafe { crate::az_mark(0x60850_u32, segments.len() as u32); } if segments.len() <= 1 {
7455 let (seg_start, seg_end, font_id) = if let Some(s) = segments.first() { unsafe { crate::az_mark(0x60854_u32, 0x0000_0001_u32); } s } else {
7457 unsafe { crate::az_mark(0x60854_u32, 0x0000_00EE_u32); } if dbg {
7459 eprintln!("[FONT FALLBACK] no font could render any char in '{}'", text.chars().take(20).collect::<String>());
7460 }
7461 return Ok(Vec::new());
7462 };
7463 let font = if let Some(f) = loaded_fonts.get(font_id) { unsafe { crate::az_mark(0x60858_u32, 0x0000_0001_u32); } f } else {
7464 unsafe { crate::az_mark(0x60858_u32, 0x0000_00EE_u32); } if dbg {
7466 eprintln!("[FONT FALLBACK] font {:?} not in loaded_fonts for '{}'", font_id, text.chars().take(20).collect::<String>());
7467 }
7468 return Ok(Vec::new());
7469 };
7470 if *seg_start == 0 && *seg_end == text.len() {
7472 unsafe { crate::az_mark(0x60860_u32, 0xC0DE_0860_u32); } return shape_text_correctly(
7474 text, script, language, direction,
7475 font, style, source_index, source_node_id,
7476 );
7477 }
7478 let mut clusters = shape_text_correctly(
7479 &text[*seg_start..*seg_end], script, language, direction,
7480 font, style, source_index, source_node_id,
7481 )?;
7482 if *seg_start > 0 {
7483 for cluster in &mut clusters {
7484 cluster.source_cluster_id.start_byte_in_run += *seg_start as u32;
7485 }
7486 }
7487 return Ok(clusters);
7488 }
7489
7490 let mut all_clusters = Vec::new();
7492 for (seg_start, seg_end, font_id) in &segments {
7493 let Some(font) = loaded_fonts.get(font_id) else {
7494 if dbg {
7495 eprintln!("[FONT FALLBACK] font {font_id:?} NOT loaded, skipping segment bytes {seg_start}..{seg_end}");
7496 }
7497 continue;
7498 };
7499 let segment_text = &text[*seg_start..*seg_end];
7500 if dbg {
7501 eprintln!(
7502 "[FONT FALLBACK] text='{segment_text}' uses font {font_id:?} (bytes {seg_start}..{seg_end})"
7503 );
7504 }
7505 let mut seg_clusters = shape_text_correctly(
7506 segment_text, script, language, direction,
7507 font, style, source_index, source_node_id,
7508 )?;
7509 if *seg_start > 0 {
7512 for cluster in &mut seg_clusters {
7513 cluster.source_cluster_id.start_byte_in_run += *seg_start as u32;
7514 }
7515 }
7516 all_clusters.extend(seg_clusters);
7517 }
7518 Ok(all_clusters)
7519}
7520
7521#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] #[allow(clippy::implicit_hasher)] #[allow(clippy::match_same_arms)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn shape_visual_items<T: ParsedFontTrait>(
7529 visual_items: &[VisualItem],
7530 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
7531 fc_cache: &FcFontCache,
7532 loaded_fonts: &LoadedFonts<T>,
7533 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
7534) -> Result<Vec<ShapedItem>, LayoutError> {
7535 let mut shaped = Vec::new();
7536 let mut idx = 0;
7537 let mut _coalesced_runs = 0usize;
7538 let mut _total_runs = 0usize;
7539 let mut _shape_calls = 0usize;
7540
7541 while idx < visual_items.len() {
7544 let item = &visual_items[idx];
7545 match &item.logical_source {
7546 LogicalItem::Text {
7547 style,
7548 source,
7549 marker_position_outside,
7550 source_node_id,
7551 ..
7552 } => {
7553 let layout_hash = style.layout_hash();
7554 let bidi_level = item.bidi_level;
7555 let script = item.script;
7556
7557 let mut coalesce_end = idx + 1;
7563 while coalesce_end < visual_items.len() {
7564 let next = &visual_items[coalesce_end];
7565 if let LogicalItem::Text { style: next_style, .. } = &next.logical_source {
7566 if next_style.layout_hash() == layout_hash
7567 && next.bidi_level == bidi_level
7568 && next.script == script
7569 {
7570 coalesce_end += 1;
7571 } else {
7572 break;
7573 }
7574 } else {
7575 break;
7576 }
7577 }
7578
7579 let coalesce_count = coalesce_end - idx;
7580
7581 if coalesce_count > 1 {
7582 _coalesced_runs += coalesce_count;
7583 _shape_calls += 1;
7584 let total_text_len: usize = visual_items[idx..coalesce_end]
7590 .iter()
7591 .map(|v| v.text.len())
7592 .sum();
7593 let mut merged_text = String::with_capacity(total_text_len);
7594 let mut byte_ranges: Vec<(
7596 usize, usize,
7597 Arc<StyleProperties>,
7598 ContentIndex,
7599 Option<NodeId>,
7600 Option<bool>,
7601 usize,
7602 )> = Vec::with_capacity(coalesce_count);
7603
7604 for item in &visual_items[idx..coalesce_end] {
7605 let start = merged_text.len();
7606 merged_text.push_str(&item.text);
7607 let end = merged_text.len();
7608 if let LogicalItem::Text {
7609 style: s, source: src, source_node_id: nid,
7610 marker_position_outside: mpo, ..
7611 } = &item.logical_source {
7612 byte_ranges.push((start, end, s.clone(), *src, *nid, *mpo, item.run_byte_offset));
7613 }
7614 }
7615
7616 if let Some(msgs) = debug_messages {
7617 msgs.push(LayoutDebugMessage::info(format!(
7618 "[TextLayout] Coalescing {} text runs ({} bytes) into single shaping call",
7619 coalesce_count, merged_text.len()
7620 )));
7621 }
7622
7623 let direction = if bidi_level.is_rtl() {
7624 BidiDirection::Rtl
7625 } else {
7626 BidiDirection::Ltr
7627 };
7628 let language = script_to_language(script, &merged_text);
7629
7630 let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
7633 FontStack::Ref(font_ref) => {
7634 shape_text_correctly(
7635 &merged_text, script, language, direction,
7636 font_ref, style, *source, *source_node_id,
7637 )
7638 }
7639 FontStack::Stack(selectors) => {
7640 let cache_key = FontChainKey::from_selectors(selectors);
7641 let Some(font_chain) = font_chain_cache.get(&cache_key) else { idx = coalesce_end; continue; };
7642 shape_with_font_fallback(
7644 &merged_text, script, language, direction,
7645 style, *source, *source_node_id,
7646 font_chain, fc_cache, loaded_fonts,
7647 )
7648 }
7649 };
7650
7651 let shaped_clusters = shaped_clusters_result?;
7652
7653 for cluster in shaped_clusters {
7658 let byte_pos = cluster.source_cluster_id.start_byte_in_run as usize;
7659 let orig = byte_ranges.iter().find(|(start, end, ..)| {
7661 byte_pos >= *start && byte_pos < *end
7662 });
7663 let mut cluster = cluster;
7664 if let Some((range_start, _, orig_style, orig_source, orig_nid, orig_mpo, orig_run_offset)) = orig {
7665 cluster.style = orig_style.clone();
7667 cluster.source_content_index = *orig_source;
7668 cluster.source_node_id = *orig_nid;
7669 cluster.source_cluster_id.source_run = orig_source.run_index;
7673 cluster.source_cluster_id.start_byte_in_run = (byte_pos - range_start + *orig_run_offset) as u32;
7674 for glyph in &mut cluster.glyphs {
7676 glyph.style = orig_style.clone();
7677 }
7678 if let Some(is_outside) = orig_mpo {
7679 cluster.marker_position_outside = Some(*is_outside);
7680 }
7681 }
7682 shaped.push(ShapedItem::Cluster(cluster));
7683 }
7684
7685 idx = coalesce_end;
7686 continue;
7687 }
7688
7689 _total_runs += 1;
7691 _shape_calls += 1;
7692 let direction = if item.bidi_level.is_rtl() {
7693 BidiDirection::Rtl
7694 } else {
7695 BidiDirection::Ltr
7696 };
7697
7698 let language = script_to_language(item.script, &item.text);
7699
7700 let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
7702 FontStack::Ref(font_ref) => {
7703 unsafe { crate::az_mark(0x60820_u32, 0x0000_0001_u32); } if let Some(msgs) = debug_messages {
7706 msgs.push(LayoutDebugMessage::info(format!(
7707 "[TextLayout] Using direct FontRef for text: '{}'",
7708 item.text.chars().take(30).collect::<String>()
7709 )));
7710 }
7711 shape_text_correctly(
7712 &item.text,
7713 item.script,
7714 language,
7715 direction,
7716 font_ref,
7717 style,
7718 *source,
7719 *source_node_id,
7720 )
7721 }
7722 FontStack::Stack(selectors) => {
7723 unsafe { crate::az_mark(0x60820_u32, 0x0000_0002_u32); } let cache_key = FontChainKey::from_selectors(selectors);
7726 unsafe { crate::az_mark(0x60824_u32, font_chain_cache.len() as u32); } let Some(font_chain) = font_chain_cache.get(&cache_key) else {
7734 if let Some(msgs) = debug_messages {
7735 msgs.push(LayoutDebugMessage::warning(format!(
7736 "[TextLayout] Font chain not pre-resolved for {:?} - text will \
7737 not be rendered",
7738 cache_key.font_families
7739 )));
7740 }
7741 idx += 1;
7742 continue;
7743 };
7744
7745 shape_with_font_fallback(
7747 &item.text, item.script, language, direction,
7748 style, *source, *source_node_id,
7749 font_chain, fc_cache, loaded_fonts,
7750 )
7751 }
7752 };
7753
7754 let mut shaped_clusters = shaped_clusters_result?;
7755
7756 let run_byte_offset = item.run_byte_offset as u32;
7761 if run_byte_offset != 0 {
7762 for cluster in &mut shaped_clusters {
7763 cluster.source_cluster_id.start_byte_in_run = cluster
7764 .source_cluster_id
7765 .start_byte_in_run
7766 .saturating_add(run_byte_offset);
7767 }
7768 }
7769
7770 if let Some(is_outside) = marker_position_outside {
7772 for cluster in &mut shaped_clusters {
7773 cluster.marker_position_outside = Some(*is_outside);
7774 }
7775 }
7776
7777 shaped.extend(shaped_clusters.into_iter().map(ShapedItem::Cluster));
7778 }
7779 LogicalItem::Tab { source, style } => {
7786 if style.tab_size == 0.0 {
7787 shaped.push(ShapedItem::Tab {
7789 source: *source,
7790 bounds: Rect {
7791 x: 0.0,
7792 y: 0.0,
7793 width: 0.0,
7794 height: 0.0,
7795 },
7796 });
7797 } else {
7798 let space_advance_approx = style.font_size_px * SPACE_WIDTH_RATIO;
7802 let ls = style.letter_spacing.resolve_px(style.font_size_px);
7804 let ws = style.word_spacing.resolve_px(style.font_size_px);
7805 let tab_interval = style.tab_size * (space_advance_approx + ls + ws);
7807 let current_advance: f32 = shaped.iter().map(|item| {
7809 match item {
7810 ShapedItem::Cluster(c) => c.advance,
7811 ShapedItem::Tab { bounds, .. } => bounds.width,
7812 ShapedItem::Object { bounds, .. } => bounds.width,
7813 _ => 0.0,
7814 }
7815 }).sum();
7816 let next_tab_stop = ((current_advance / tab_interval).floor() + 1.0) * tab_interval;
7818 let mut tab_width = next_tab_stop - current_advance;
7819 let half_ch = space_advance_approx * 0.5;
7821 if tab_width < half_ch {
7822 tab_width += tab_interval;
7823 }
7824 shaped.push(ShapedItem::Tab {
7825 source: *source,
7826 bounds: Rect {
7827 x: 0.0,
7828 y: 0.0,
7829 width: tab_width,
7830 height: 0.0,
7831 },
7832 });
7833 }
7834 }
7835 LogicalItem::Ruby {
7836 source,
7837 base_text,
7838 ruby_text,
7839 style,
7840 } => {
7841 let base_font_size = style.font_size_px;
7850 let annotation_font_size = base_font_size * RUBY_ANNOTATION_FONT_SCALE;
7851
7852 let mut annotation_props = (**style).clone();
7853 annotation_props.font_size_px = annotation_font_size;
7854 let annotation_style = Arc::new(annotation_props);
7855
7856 let base_width = measure_run_advance(
7859 base_text, style, item.script, *source, font_chain_cache, fc_cache,
7860 loaded_fonts,
7861 )
7862 .unwrap_or_else(|| base_text.chars().count() as f32 * base_font_size);
7863 let annotation_width = measure_run_advance(
7864 ruby_text, &annotation_style, item.script, *source, font_chain_cache,
7865 fc_cache, loaded_fonts,
7866 )
7867 .unwrap_or_else(|| ruby_text.chars().count() as f32 * annotation_font_size);
7868
7869 let base_line_height =
7870 style.line_height.resolve(base_font_size, 0.0, 0.0, 0.0, 0);
7871 let annotation_line_height = annotation_style.line_height.resolve(
7872 annotation_font_size, 0.0, 0.0, 0.0, 0,
7873 );
7874 let (reserved_width, reserved_height) = ruby_reserved_box(
7877 base_width,
7878 annotation_width,
7879 base_line_height,
7880 annotation_line_height,
7881 );
7882
7883 shaped.push(ShapedItem::Object {
7889 source: *source,
7890 bounds: Rect {
7891 x: 0.0,
7892 y: 0.0,
7893 width: reserved_width,
7894 height: reserved_height,
7895 },
7896 baseline_offset: 0.0,
7897 content: InlineContent::Text(StyledRun {
7898 text: base_text.clone(),
7899 style: style.clone(),
7900 logical_start_byte: 0,
7901 source_node_id: None,
7902 }),
7903 });
7904 }
7905 LogicalItem::CombinedText {
7906 style,
7907 source,
7908 text,
7909 } => {
7910 let language = script_to_language(item.script, &item.text);
7911
7912 let text = if text.chars().count() > 1 {
7918 let converted: String = text.chars().map(|c| {
7919 let cp = c as u32;
7920 if (0xFF01..=0xFF5E).contains(&cp) {
7921 char::from_u32(cp - 0xFF01 + 0x0021).unwrap_or(c)
7923 } else {
7924 c
7925 }
7926 }).collect();
7927 converted
7928 } else {
7929 text.clone()
7930 };
7931
7932 let glyphs: Vec<Glyph> = match &style.font_stack {
7937 FontStack::Ref(font_ref) => {
7938 if let Some(msgs) = debug_messages {
7940 msgs.push(LayoutDebugMessage::info(format!(
7941 "[TextLayout] Using direct FontRef for CombinedText: '{}'",
7942 text.chars().take(30).collect::<String>()
7943 )));
7944 }
7945 font_ref.shape_text(
7946 &text,
7947 item.script,
7948 language,
7949 BidiDirection::Ltr,
7950 style.as_ref(),
7951 )?
7952 }
7953 FontStack::Stack(selectors) => {
7954 let cache_key = FontChainKey::from_selectors(selectors);
7956
7957 let Some(font_chain) = font_chain_cache.get(&cache_key) else {
7958 if let Some(msgs) = debug_messages {
7959 msgs.push(LayoutDebugMessage::warning(format!(
7960 "[TextLayout] Font chain not pre-resolved for CombinedText {:?}",
7961 cache_key.font_families
7962 )));
7963 }
7964 idx += 1;
7965 continue;
7966 };
7967
7968 let segments = split_text_by_font_coverage(&text, font_chain, fc_cache, loaded_fonts);
7970 let mut all_glyphs = Vec::new();
7971 for (seg_start, seg_end, font_id) in &segments {
7972 let Some(font) = loaded_fonts.get(font_id) else { continue; };
7973 let segment_text = &text[*seg_start..*seg_end];
7974 let mut seg_glyphs = font.shape_text(
7975 segment_text,
7976 item.script,
7977 language,
7978 BidiDirection::Ltr,
7979 style.as_ref(),
7980 )?;
7981 if *seg_start > 0 {
7983 for g in &mut seg_glyphs {
7984 g.logical_byte_index += *seg_start;
7985 g.cluster += *seg_start as u32;
7986 }
7987 }
7988 all_glyphs.extend(seg_glyphs);
7989 }
7990 if all_glyphs.is_empty() {
7991 idx += 1;
7992 continue;
7993 }
7994 all_glyphs
7995 }
7996 };
7997
7998 let shaped_glyphs: ShapedGlyphVec = glyphs
7999 .into_iter()
8000 .map(|g| ShapedGlyph {
8001 kind: GlyphKind::Character,
8002 glyph_id: g.glyph_id,
8003 script: g.script,
8004 font_hash: g.font_hash,
8005 font_metrics: g.font_metrics,
8006 style: g.style,
8007 cluster_offset: 0,
8008 advance: g.advance,
8009 kerning: g.kerning,
8010 offset: g.offset,
8011 vertical_advance: g.vertical_advance,
8012 vertical_offset: g.vertical_bearing,
8013 })
8014 .collect();
8015
8016 let total_width: f32 = shaped_glyphs.iter().map(|g| g.advance + g.kerning).sum();
8018 let em_size = shaped_glyphs.first()
8024 .map_or(style.font_size_px, |g| g.style.font_size_px);
8025 let bounds = Rect {
8026 x: 0.0,
8027 y: 0.0,
8028 width: total_width,
8029 height: em_size,
8030 };
8031
8032 shaped.push(ShapedItem::CombinedBlock {
8033 source: *source,
8034 glyphs: shaped_glyphs,
8035 bounds,
8036 baseline_offset: em_size / 2.0,
8037 });
8038 }
8039 LogicalItem::Object {
8040 content, source, ..
8041 } => {
8042 let (bounds, baseline) = measure_inline_object(content)?;
8043 shaped.push(ShapedItem::Object {
8044 source: *source,
8045 bounds,
8046 baseline_offset: baseline,
8047 content: content.clone(),
8048 });
8049 }
8050 LogicalItem::Break { source, break_info } => {
8051 shaped.push(ShapedItem::Break {
8052 source: *source,
8053 break_info: *break_info,
8054 });
8055 }
8056 }
8057 idx += 1;
8058 }
8059
8060 Ok(shaped)
8061}
8062
8063const fn is_hanging_punctuation_char(c: char) -> bool {
8066 matches!(c,
8067 ',' | '.' | '\u{060C}' | '\u{06D4}' | '\u{3001}' | '\u{3002}' | '\u{FF0C}' | '\u{FF0E}' | '\u{FE50}' | '\u{FE51}' | '\u{FE52}' | '\u{FF61}' | '\u{FF64}' )
8081}
8082
8083fn is_hanging_punctuation(item: &ShapedItem) -> bool {
8088 if let ShapedItem::Cluster(c) = item {
8089 if c.glyphs.len() == 1 {
8090 c.text.chars().next().is_some_and(is_hanging_punctuation_char)
8091 } else {
8092 false
8093 }
8094 } else {
8095 false
8096 }
8097}
8098
8099#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] fn shape_text_correctly<T: ParsedFontTrait>(
8102 text: &str,
8103 script: Script,
8104 language: Language,
8105 direction: BidiDirection,
8106 font: &T, style: &Arc<StyleProperties>,
8108 source_index: ContentIndex,
8109 source_node_id: Option<NodeId>,
8110) -> Result<Vec<ShapedCluster>, LayoutError> {
8111 unsafe { crate::az_mark(0x60864_u32, 0xC0DE_0864_u32); } let glyphs = font.shape_text(text, script, language, direction, style.as_ref())?;
8113 unsafe { crate::az_mark(0x60868_u32, (glyphs.len() as u32) | 0x8000_0000_u32); } if glyphs.is_empty() {
8116 return Ok(Vec::new());
8117 }
8118
8119 let mut clusters = Vec::new();
8120
8121 let mut current_cluster_glyphs = Vec::new();
8123 let mut cluster_id = glyphs[0].cluster;
8124 let mut cluster_start_byte_in_text = glyphs[0].logical_byte_index;
8125
8126 for glyph in glyphs {
8127 if glyph.cluster != cluster_id {
8128 let advance = current_cluster_glyphs
8130 .iter()
8131 .map(|g: &Glyph| g.advance)
8132 .sum();
8133
8134 let (start, end) = if cluster_start_byte_in_text <= glyph.logical_byte_index {
8137 (cluster_start_byte_in_text, glyph.logical_byte_index)
8138 } else {
8139 (glyph.logical_byte_index, cluster_start_byte_in_text)
8140 };
8141 let cluster_text = text.get(start..end).unwrap_or("");
8142
8143 clusters.push(ShapedCluster {
8144 text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
8146 source_run: source_index.run_index,
8147 start_byte_in_run: cluster_id,
8148 },
8149 source_content_index: source_index,
8150 source_node_id,
8151 glyphs: current_cluster_glyphs
8152 .iter()
8153 .map(|g| {
8154 let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
8156 (g.logical_byte_index - cluster_start_byte_in_text) as u32
8157 } else {
8158 0
8159 };
8160 ShapedGlyph {
8161 kind: if g.glyph_id == 0 {
8162 GlyphKind::NotDef
8163 } else {
8164 GlyphKind::Character
8165 },
8166 glyph_id: g.glyph_id,
8167 script: g.script,
8168 font_hash: g.font_hash,
8169 font_metrics: g.font_metrics,
8170 style: g.style.clone(),
8171 cluster_offset,
8172 advance: g.advance,
8173 kerning: g.kerning,
8174 vertical_advance: g.vertical_advance,
8175 vertical_offset: g.vertical_bearing,
8176 offset: g.offset,
8177 }
8178 })
8179 .collect(),
8180 advance,
8181 direction,
8182 style: style.clone(),
8183 marker_position_outside: None,
8184 is_first_fragment: true,
8185 is_last_fragment: true,
8186 });
8187 current_cluster_glyphs.clear();
8188 cluster_id = glyph.cluster;
8189 cluster_start_byte_in_text = glyph.logical_byte_index;
8190 }
8191 current_cluster_glyphs.push(glyph);
8192 }
8193
8194 if !current_cluster_glyphs.is_empty() {
8196 let advance = current_cluster_glyphs
8197 .iter()
8198 .map(|g: &Glyph| g.advance)
8199 .sum();
8200 let cluster_text = text.get(cluster_start_byte_in_text..).unwrap_or("");
8201 clusters.push(ShapedCluster {
8202 text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
8204 source_run: source_index.run_index,
8205 start_byte_in_run: cluster_id,
8206 },
8207 source_content_index: source_index,
8208 source_node_id,
8209 glyphs: current_cluster_glyphs
8210 .iter()
8211 .map(|g| {
8212 let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
8214 (g.logical_byte_index - cluster_start_byte_in_text) as u32
8215 } else {
8216 0
8217 };
8218 ShapedGlyph {
8219 kind: if g.glyph_id == 0 {
8220 GlyphKind::NotDef
8221 } else {
8222 GlyphKind::Character
8223 },
8224 glyph_id: g.glyph_id,
8225 font_hash: g.font_hash,
8226 font_metrics: g.font_metrics,
8227 style: g.style.clone(),
8228 script: g.script,
8229 vertical_advance: g.vertical_advance,
8230 vertical_offset: g.vertical_bearing,
8231 cluster_offset,
8232 advance: g.advance,
8233 kerning: g.kerning,
8234 offset: g.offset,
8235 }
8236 })
8237 .collect(),
8238 advance,
8239 direction,
8240 style: style.clone(),
8241 marker_position_outside: None,
8242 is_first_fragment: true,
8243 is_last_fragment: true,
8244 });
8245 }
8246
8247 Ok(clusters)
8248}
8249
8250fn measure_inline_object(item: &InlineContent) -> Result<(Rect, f32), LayoutError> {
8252 match item {
8253 InlineContent::Image(img) => {
8254 let size = img.display_size.unwrap_or(img.intrinsic_size);
8255 Ok((
8256 Rect {
8257 x: 0.0,
8258 y: 0.0,
8259 width: size.width,
8260 height: size.height,
8261 },
8262 img.baseline_offset,
8263 ))
8264 }
8265 InlineContent::Shape(shape) => Ok({
8266 let size = shape.shape_def.get_size();
8267 (
8268 Rect {
8269 x: 0.0,
8270 y: 0.0,
8271 width: size.width,
8272 height: size.height,
8273 },
8274 shape.baseline_offset,
8275 )
8276 }),
8277 InlineContent::Space(space) => Ok((
8278 Rect {
8279 x: 0.0,
8280 y: 0.0,
8281 width: space.width,
8282 height: 0.0,
8283 },
8284 0.0,
8285 )),
8286 InlineContent::Marker { .. } => {
8287 Err(LayoutError::InvalidText(
8289 "Marker is text content, not a measurable object".into(),
8290 ))
8291 }
8292 _ => Err(LayoutError::InvalidText("Not a measurable object".into())),
8293 }
8294}
8295
8296fn apply_text_orientation(
8302 items: Arc<Vec<ShapedItem>>,
8303 constraints: &UnifiedConstraints,
8304) -> Arc<Vec<ShapedItem>> {
8305 if !constraints.is_vertical() {
8306 return items;
8307 }
8308
8309 let mut oriented_items = Vec::with_capacity(items.len());
8310 let writing_mode = constraints.writing_mode.unwrap_or_default();
8311
8312 for item in items.iter() {
8313 match item {
8314 ShapedItem::Cluster(cluster) => {
8315 let mut new_cluster = cluster.clone();
8316 let mut total_vertical_advance = 0.0;
8317
8318 for glyph in &mut new_cluster.glyphs {
8319 if glyph.vertical_advance > 0.0 {
8322 total_vertical_advance += glyph.vertical_advance;
8323 } else {
8324 let fallback_advance = cluster.style.line_height.resolve_with_metrics(cluster.style.font_size_px, &glyph.font_metrics);
8326 glyph.vertical_advance = fallback_advance;
8327 glyph.vertical_offset = Point {
8329 x: -glyph.advance / 2.0,
8330 y: 0.0,
8331 };
8332 total_vertical_advance += fallback_advance;
8333 }
8334 }
8335 new_cluster.advance = total_vertical_advance;
8337 oriented_items.push(ShapedItem::Cluster(new_cluster));
8338 }
8339 ShapedItem::Object {
8341 source,
8342 bounds,
8343 baseline_offset,
8344 content,
8345 } => {
8346 let mut new_bounds = *bounds;
8347 std::mem::swap(&mut new_bounds.width, &mut new_bounds.height);
8348 oriented_items.push(ShapedItem::Object {
8349 source: *source,
8350 bounds: new_bounds,
8351 baseline_offset: *baseline_offset,
8352 content: content.clone(),
8353 });
8354 }
8355 _ => oriented_items.push(item.clone()),
8356 }
8357 }
8358
8359 Arc::new(oriented_items)
8360}
8361
8362fn get_item_vertical_align(item: &ShapedItem) -> Option<VerticalAlign> {
8371 match item {
8372 ShapedItem::Object { content, .. } => match content {
8373 InlineContent::Image(img) => Some(img.alignment),
8374 InlineContent::Shape(shape) => Some(shape.alignment),
8375 _ => None,
8376 },
8377 ShapedItem::Cluster(c) => match c.style.vertical_align {
8382 VerticalAlign::Baseline => None,
8383 va => Some(va),
8384 },
8385 _ => None,
8386 }
8387}
8388
8389#[allow(clippy::match_same_arms)] #[must_use] pub fn get_item_vertical_metrics_approx(item: &ShapedItem) -> (f32, f32) {
8393 if let ShapedItem::Cluster(c) = item {
8395 if !c.glyphs.is_empty() {
8396 let (asc, desc) = c.glyphs
8398 .iter()
8399 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
8400 let metrics = &glyph.font_metrics;
8401 if metrics.units_per_em == 0 {
8402 return (max_asc, max_desc);
8403 }
8404 let scale = glyph.style.font_size_px / f32::from(metrics.units_per_em);
8405 let font_ascent = metrics.ascent * scale;
8406 let font_descent = (-metrics.descent * scale).max(0.0);
8407 let ad = font_ascent + font_descent;
8408 let resolved_lh = c.style.line_height.resolve_with_metrics(glyph.style.font_size_px, &glyph.font_metrics);
8409 let half_leading = (resolved_lh - ad) / 2.0;
8410 (max_asc.max(font_ascent + half_leading), max_desc.max(font_descent + half_leading))
8411 });
8412 return (asc, desc);
8413 }
8414 }
8415 match item {
8417 ShapedItem::Cluster(c) => {
8418 let lh = c.style.line_height.resolve(c.style.font_size_px, 0.0, 0.0, 0.0, 0);
8419 (lh * FALLBACK_ASCENT_RATIO, lh * FALLBACK_DESCENT_RATIO)
8420 }
8421 ShapedItem::CombinedBlock { bounds, .. } => {
8422 (bounds.height * FALLBACK_ASCENT_RATIO, bounds.height * FALLBACK_DESCENT_RATIO)
8423 }
8424 ShapedItem::Object { bounds, .. } => (bounds.height, 0.0),
8425 ShapedItem::Tab { bounds, .. } => {
8426 (bounds.height * FALLBACK_ASCENT_RATIO, bounds.height * FALLBACK_DESCENT_RATIO)
8427 }
8428 ShapedItem::Break { .. } => (0.0, 0.0),
8429 }
8430}
8431
8432#[must_use] pub fn get_item_vertical_metrics(item: &ShapedItem, constraints: &UnifiedConstraints) -> (f32, f32) {
8446 match item {
8448 ShapedItem::Cluster(c) => {
8449 if c.glyphs.is_empty() {
8450 let ad = constraints.strut_ascent + constraints.strut_descent;
8456 let resolved_lh = c.style.line_height.resolve(c.style.font_size_px, 0.0, 0.0, 0.0, 0);
8457 let half_leading = (resolved_lh - ad) / 2.0;
8458 return (constraints.strut_ascent + half_leading, constraints.strut_descent + half_leading);
8459 }
8460 c.glyphs
8473 .iter()
8474 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
8475 let metrics = &glyph.font_metrics;
8476 if metrics.units_per_em == 0 {
8477 return (max_asc, max_desc);
8478 }
8479 let scale = glyph.style.font_size_px / f32::from(metrics.units_per_em);
8480 let a = metrics.ascent * scale;
8481 let d = (-metrics.descent * scale).max(0.0);
8484 let ad = a + d;
8485 let resolved_lh = glyph.style.line_height.resolve_with_metrics(glyph.style.font_size_px, &glyph.font_metrics);
8486 let leading = resolved_lh - ad;
8487 let half_leading = leading / 2.0;
8488 let item_asc = a + half_leading;
8489 let item_desc = d + half_leading;
8490 (max_asc.max(item_asc), max_desc.max(item_desc))
8491 })
8492 }
8493 ShapedItem::Object {
8494 bounds,
8495 baseline_offset,
8496 ..
8497 } => {
8498 let ascent = bounds.height - *baseline_offset;
8501 let descent = *baseline_offset;
8502 (ascent.max(0.0), descent.max(0.0))
8503 }
8504 ShapedItem::CombinedBlock {
8505 bounds,
8506 baseline_offset,
8507 ..
8508 } => {
8509 let ascent = bounds.height - *baseline_offset;
8511 let descent = *baseline_offset;
8512 (ascent.max(0.0), descent.max(0.0))
8513 }
8514 _ => (0.0, 0.0), }
8516}
8517
8518fn calculate_line_metrics(
8532 items: &[ShapedItem],
8533 default_vertical_align: VerticalAlign,
8534 constraints: &UnifiedConstraints,
8535) -> (f32, f32) {
8536 let (mut max_asc, mut max_desc) = items
8540 .iter()
8541 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), item| {
8542 let effective_align = get_item_vertical_align(item)
8543 .unwrap_or(default_vertical_align);
8544 match effective_align {
8545 VerticalAlign::Top | VerticalAlign::Bottom => {
8546 (max_asc, max_desc)
8548 }
8549 _ => {
8550 let (item_asc, item_desc) = get_item_vertical_metrics(item, constraints);
8551 (max_asc.max(item_asc), max_desc.max(item_desc))
8552 }
8553 }
8554 });
8555
8556 let baseline_line_height = max_asc + max_desc;
8557
8558 for item in items {
8561 let effective_align = get_item_vertical_align(item)
8562 .unwrap_or(default_vertical_align);
8563 match effective_align {
8564 VerticalAlign::Top | VerticalAlign::Bottom => {
8565 let (item_asc, item_desc) = get_item_vertical_metrics(item, constraints);
8566 let item_height = item_asc + item_desc;
8567 if item_height > baseline_line_height {
8568 if effective_align == VerticalAlign::Top {
8570 max_desc = max_desc.max(item_height - max_asc);
8572 } else {
8573 max_asc = max_asc.max(item_height - max_desc);
8575 }
8576 }
8577 }
8578 _ => {} }
8580 }
8581
8582 (max_asc, max_desc)
8583}
8584
8585fn apply_l2_visual_reversal(line_items: &mut [ShapedItem]) {
8602 let dir_of = |it: &ShapedItem| it.as_cluster().map(|c| c.direction);
8603 let mut i = 0;
8604 while i < line_items.len() {
8605 let Some(dir) = dir_of(&line_items[i]) else {
8606 i += 1;
8607 continue;
8608 };
8609 let mut j = i + 1;
8610 while j < line_items.len() && dir_of(&line_items[j]) == Some(dir) {
8611 j += 1;
8612 }
8613 if dir == BidiDirection::Rtl {
8614 line_items[i..j].reverse();
8615 }
8616 i = j;
8617 }
8618}
8619
8620#[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn perform_fragment_layout<T: ParsedFontTrait>(
8662 cursor: &mut BreakCursor<'_>,
8663 logical_items: &[LogicalItem],
8664 fragment_constraints: &UnifiedConstraints,
8665 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
8666 fonts: &LoadedFonts<T>,
8667) -> Result<UnifiedLayout, LayoutError> {
8668 const MAX_EMPTY_SEGMENTS: usize = 1000; if let Some(msgs) = debug_messages {
8670 msgs.push(LayoutDebugMessage::info(
8671 "\n--- Entering perform_fragment_layout ---".to_string(),
8672 ));
8673 msgs.push(LayoutDebugMessage::info(format!(
8674 "Constraints: available_width={:?}, available_height={:?}, columns={}, text_wrap={:?}",
8675 fragment_constraints.available_width,
8676 fragment_constraints.available_height,
8677 fragment_constraints.columns,
8678 fragment_constraints.text_wrap
8679 )));
8680 }
8681
8682 if fragment_constraints.text_wrap == TextWrap::Balance {
8685 if let Some(msgs) = debug_messages {
8686 msgs.push(LayoutDebugMessage::info(
8687 "Using Knuth-Plass algorithm for text-wrap: balance".to_string(),
8688 ));
8689 }
8690
8691 let shaped_items: Vec<ShapedItem> = cursor.drain_remaining();
8693
8694 let hyphenator = if fragment_constraints.hyphenation == Hyphens::Auto {
8696 fragment_constraints
8697 .hyphenation_language
8698 .and_then(|lang| get_hyphenator(lang).ok())
8699 } else {
8700 None
8701 };
8702
8703 return Ok(crate::text3::knuth_plass::kp_layout(
8705 &shaped_items,
8706 logical_items,
8707 fragment_constraints,
8708 hyphenator.as_ref(),
8709 fonts,
8710 ));
8711 }
8712
8713 let hyphenator = if fragment_constraints.hyphenation == Hyphens::Auto {
8715 fragment_constraints
8716 .hyphenation_language
8717 .and_then(|lang| get_hyphenator(lang).ok())
8718 } else {
8719 None
8720 };
8721
8722 let mut positioned_items = Vec::new();
8723 let mut layout_bounds = Rect::default();
8724
8725 let num_columns = fragment_constraints.columns.max(1);
8726 let total_column_gap = fragment_constraints.column_gap * (num_columns - 1) as f32;
8727
8728 let is_min_content = matches!(fragment_constraints.available_width, AvailableSpace::MinContent);
8741 let is_max_content = matches!(fragment_constraints.available_width, AvailableSpace::MaxContent);
8742
8743 let column_width = match fragment_constraints.available_width {
8744 AvailableSpace::Definite(width) => (width - total_column_gap) / num_columns as f32,
8745 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
8746 f32::MAX / 2.0
8749 }
8750 };
8751 let mut current_column = 0;
8752 if let Some(msgs) = debug_messages {
8753 msgs.push(LayoutDebugMessage::info(format!(
8754 "Column width calculated: {column_width}"
8755 )));
8756 }
8757
8758 let base_direction = if fragment_constraints.unicode_bidi == UnicodeBidi::Plaintext {
8764 let remaining = &cursor.items[cursor.next_item_index..];
8766 let text: String = remaining.iter()
8767 .filter_map(|i| i.as_cluster())
8768 .map(|c| c.text.as_str())
8769 .collect();
8770 match unicode_bidi::get_base_direction(text.as_str()) {
8771 unicode_bidi::Direction::Ltr => BidiDirection::Ltr,
8772 unicode_bidi::Direction::Rtl => BidiDirection::Rtl,
8773 unicode_bidi::Direction::Mixed => fragment_constraints.direction.unwrap_or(BidiDirection::Ltr),
8775 }
8776 } else {
8777 fragment_constraints.direction.unwrap_or(BidiDirection::Ltr)
8778 };
8779
8780 if let Some(msgs) = debug_messages {
8781 msgs.push(LayoutDebugMessage::info(format!(
8782 "[PFLayout] Base direction: {:?} (from CSS), Text align: {:?}",
8783 base_direction, fragment_constraints.text_align
8784 )));
8785 }
8786
8787 let balanced_lines_per_column: Option<usize> = if num_columns > 1
8799 && fragment_constraints.shape_boundaries.is_empty()
8800 && !is_min_content
8801 && !is_max_content
8802 {
8803 let mut probe = cursor.clone();
8804 let mut probe_col_constraints = fragment_constraints.clone();
8805 probe_col_constraints.available_width = AvailableSpace::Definite(column_width);
8806 let probe_line_height = fragment_constraints.resolved_line_height();
8807 let iter_cap = probe.items.len().saturating_mul(4).max(64);
8809 let mut total_lines = 0usize;
8810 let mut probe_y = 0.0_f32;
8811 let mut probe_guard = 0usize;
8812 while !probe.is_done() && probe_guard < iter_cap {
8813 probe_guard += 1;
8814 let lc = get_line_constraints(probe_y, probe_line_height, &probe_col_constraints, &mut None);
8815 if lc.segments.is_empty() {
8816 break;
8817 }
8818 let (probe_line, _) = break_one_line(
8819 &mut probe,
8820 &lc,
8821 false,
8822 hyphenator.as_ref(),
8823 fonts,
8824 fragment_constraints.line_break,
8825 fragment_constraints.white_space_mode,
8826 fragment_constraints.overflow_wrap,
8827 );
8828 if probe_line.is_empty() {
8829 break;
8830 }
8831 total_lines += 1;
8832 probe_y += probe_line_height;
8833 }
8834 (total_lines > 0).then(|| total_lines.div_ceil(num_columns as usize).max(1))
8835 } else {
8836 None
8837 };
8838
8839 'column_loop: while current_column < num_columns {
8840 if let Some(msgs) = debug_messages {
8841 msgs.push(LayoutDebugMessage::info(format!(
8842 "\n-- Starting Column {current_column} --"
8843 )));
8844 }
8845 let column_start_x =
8846 (column_width + fragment_constraints.column_gap) * current_column as f32;
8847 let mut line_top_y = 0.0;
8848 let mut line_index = 0;
8849 let mut empty_segment_count = 0; let mut is_after_forced_break = false;
8851 let column_item_start = positioned_items.len();
8856 let mut line_bands: Vec<(usize, f32, f32)> = Vec::new();
8857
8858 #[allow(clippy::no_effect_underscore_binding)] let mut _az_line_iters: usize = 0;
8866 while !cursor.is_done() {
8867 #[cfg(feature = "web_lift")]
8868 {
8869 _az_line_iters += 1;
8870 unsafe { crate::az_mark((0x60BC4) as u32, (_az_line_iters as u32 | 0xC0DE0000) as u32); }
8871 if _az_line_iters > 4096 {
8872 break;
8873 }
8874 }
8875 if let Some(max_height) = fragment_constraints.available_height {
8876 if line_top_y >= max_height {
8877 if let Some(msgs) = debug_messages {
8878 msgs.push(LayoutDebugMessage::info(format!(
8879 " Column full (pen {line_top_y} >= height {max_height}), breaking to next column."
8880 )));
8881 }
8882 break;
8883 }
8884 }
8885
8886 if let Some(clamp) = fragment_constraints.line_clamp {
8887 if line_index >= clamp.get() {
8888 break;
8889 }
8890 }
8891
8892 if let Some(budget) = balanced_lines_per_column {
8896 if current_column + 1 < num_columns && line_index >= budget {
8897 break;
8898 }
8899 }
8900
8901 let mut column_constraints = fragment_constraints.clone();
8903 if is_min_content {
8906 column_constraints.available_width = AvailableSpace::MinContent;
8907 } else if is_max_content {
8908 column_constraints.available_width = AvailableSpace::MaxContent;
8909 } else {
8910 column_constraints.available_width = AvailableSpace::Definite(column_width);
8911 }
8912 let line_constraints = get_line_constraints(
8913 line_top_y,
8914 fragment_constraints.resolved_line_height(),
8915 &column_constraints,
8916 debug_messages,
8917 );
8918
8919 if line_constraints.segments.is_empty() {
8920 empty_segment_count += 1;
8921 if let Some(msgs) = debug_messages {
8922 msgs.push(LayoutDebugMessage::info(format!(
8923 " No available segments at y={line_top_y}, skipping to next line. (empty count: \
8924 {empty_segment_count}/{MAX_EMPTY_SEGMENTS})"
8925 )));
8926 }
8927
8928 if empty_segment_count >= MAX_EMPTY_SEGMENTS {
8930 if let Some(msgs) = debug_messages {
8931 msgs.push(LayoutDebugMessage::warning(format!(
8932 " [WARN] Reached maximum empty segment count ({MAX_EMPTY_SEGMENTS}). Breaking to \
8933 prevent infinite loop."
8934 )));
8935 msgs.push(LayoutDebugMessage::warning(
8936 " This likely means the shape constraints are too restrictive or \
8937 positioned incorrectly."
8938 .to_string(),
8939 ));
8940 msgs.push(LayoutDebugMessage::warning(format!(
8941 " Current y={line_top_y}, shape boundaries might be outside this range."
8942 )));
8943 }
8944 break;
8945 }
8946
8947 if !fragment_constraints.shape_boundaries.is_empty() && empty_segment_count > 50 {
8950 let max_shape_y: f32 = fragment_constraints
8952 .shape_boundaries
8953 .iter()
8954 .map(|shape| {
8955 match shape {
8956 ShapeBoundary::Circle { center, radius } => center.y + radius,
8957 ShapeBoundary::Ellipse { center, radii } => center.y + radii.height,
8958 ShapeBoundary::Polygon { points } => {
8959 points.iter().map(|p| p.y).fold(0.0, f32::max)
8960 }
8961 ShapeBoundary::Rectangle(rect) => rect.y + rect.height,
8962 ShapeBoundary::Path { segments } => segments
8963 .iter()
8964 .filter_map(|s| match s {
8965 PathSegment::MoveTo(p) | PathSegment::LineTo(p) => Some(p.y),
8966 PathSegment::CurveTo { end, .. }
8967 | PathSegment::QuadTo { end, .. } => Some(end.y),
8968 PathSegment::Arc { center, radius, .. } => {
8969 Some(center.y + radius)
8970 }
8971 PathSegment::Close => None,
8972 })
8973 .fold(0.0, f32::max),
8974 }
8975 })
8976 .fold(0.0, f32::max);
8977
8978 if line_top_y > max_shape_y + 100.0 {
8979 if let Some(msgs) = debug_messages {
8980 msgs.push(LayoutDebugMessage::info(format!(
8981 " [INFO] Current y={line_top_y} is far beyond maximum shape extent y={max_shape_y}. \
8982 Breaking layout."
8983 )));
8984 msgs.push(LayoutDebugMessage::info(
8985 " Shape boundaries exist but no segments available - text cannot \
8986 fit in shape."
8987 .to_string(),
8988 ));
8989 }
8990 break;
8991 }
8992 }
8993
8994 line_top_y += fragment_constraints.resolved_line_height();
8995 continue;
8996 }
8997
8998 empty_segment_count = 0;
9000
9001 let effective_overflow_wrap = if is_min_content && fragment_constraints.overflow_wrap == OverflowWrap::Anywhere {
9006 OverflowWrap::Anywhere
9007 } else if is_min_content && fragment_constraints.overflow_wrap == OverflowWrap::BreakWord {
9008 OverflowWrap::Normal
9009 } else {
9010 fragment_constraints.overflow_wrap
9011 };
9012
9013 let (mut line_items, was_hyphenated) =
9020 break_one_line(cursor, &line_constraints, false, hyphenator.as_ref(), fonts, fragment_constraints.line_break, fragment_constraints.white_space_mode, effective_overflow_wrap);
9021 if line_items.is_empty() {
9022 if let Some(msgs) = debug_messages {
9023 msgs.push(LayoutDebugMessage::info(
9024 " Break returned no items. Ending column.".to_string(),
9025 ));
9026 }
9027 break;
9028 }
9029
9030 let line_text_before_rev: String = line_items
9031 .iter()
9032 .filter_map(|i| i.as_cluster())
9033 .map(|c| c.text.as_str())
9034 .collect();
9035 if let Some(msgs) = debug_messages {
9036 msgs.push(LayoutDebugMessage::info(format!(
9037 "[PFLayout] Line items from breaker (visual order): [{line_text_before_rev}]"
9039 )));
9040 }
9041
9042 apply_l2_visual_reversal(&mut line_items);
9047
9048 if let Some(msgs) = debug_messages {
9049 let after: String = line_items
9050 .iter()
9051 .filter_map(|i| i.as_cluster())
9052 .map(|c| c.text.as_str())
9053 .collect();
9054 if after != line_text_before_rev {
9055 msgs.push(LayoutDebugMessage::info(format!(
9056 "[PFLayout] Line items after L2 reversal: [{after}]"
9057 )));
9058 }
9059 }
9060
9061 let line_ends_with_forced_break = line_items.iter().any(|item| matches!(item, ShapedItem::Break { .. }));
9063
9064 let is_last_line = cursor.is_done() && !was_hyphenated;
9066 let effective_align = resolve_effective_alignment(
9067 fragment_constraints.text_align,
9068 fragment_constraints.text_align_last,
9069 is_last_line || line_ends_with_forced_break,
9070 );
9071
9072 let (mut line_pos_items, line_height) = position_one_line(
9073 &line_items,
9074 &line_constraints,
9075 line_top_y,
9076 line_index,
9077 effective_align,
9078 base_direction,
9079 is_last_line,
9080 fragment_constraints,
9081 debug_messages,
9082 fonts,
9083 is_after_forced_break,
9084 );
9085
9086 is_after_forced_break = line_ends_with_forced_break;
9088
9089 for item in &mut line_pos_items {
9090 item.position.x += column_start_x;
9091 }
9092
9093 let band_height = line_height.max(fragment_constraints.resolved_line_height());
9095 line_bands.push((line_index, line_top_y, band_height));
9096 line_top_y += band_height;
9097 line_index += 1;
9098 positioned_items.extend(line_pos_items);
9099 }
9100
9101 if fragment_constraints.writing_mode == Some(WritingMode::VerticalRl) {
9108 let block_extent = line_top_y;
9109 for item in &mut positioned_items[column_item_start..] {
9110 if let Some(&(_, t, h)) =
9111 line_bands.iter().find(|(li, _, _)| *li == item.line_index)
9112 {
9113 item.position.x += block_extent - t - t - h;
9116 }
9117 }
9118 }
9119 current_column += 1;
9120 }
9121
9122 if let Some(msgs) = debug_messages {
9123 msgs.push(LayoutDebugMessage::info(format!(
9124 "--- Exiting perform_fragment_layout, positioned {} items ---",
9125 positioned_items.len()
9126 )));
9127 }
9128
9129 let mut layout = UnifiedLayout {
9130 items: positioned_items,
9131 overflow: OverflowInfo::default(),
9132 };
9133
9134 let calculated_bounds = layout.bounds();
9136
9137 layout.overflow.unclipped_bounds = calculated_bounds;
9143
9144 if let Some(msgs) = debug_messages {
9145 msgs.push(LayoutDebugMessage::info(format!(
9146 "--- Calculated bounds: width={}, height={} ---",
9147 calculated_bounds.width, calculated_bounds.height
9148 )));
9149 }
9150
9151 Ok(layout)
9152}
9153
9154#[allow(clippy::cognitive_complexity)] #[allow(clippy::too_many_lines)] pub fn break_one_line<T: ParsedFontTrait>(
9215 cursor: &mut BreakCursor<'_>,
9216 line_constraints: &LineConstraints,
9217 is_vertical: bool,
9218 hyphenator: Option<&Standard>,
9219 fonts: &LoadedFonts<T>,
9220 line_break: LineBreakStrictness,
9221 white_space_mode: WhiteSpaceMode,
9222 overflow_wrap: OverflowWrap,
9223) -> (Vec<ShapedItem>, bool) {
9224 let mut line_items = Vec::new();
9225 let mut current_width = 0.0;
9226
9227 if cursor.is_done() {
9228 return (Vec::new(), false);
9229 }
9230
9231 let strip_leading = matches!(
9239 white_space_mode,
9240 WhiteSpaceMode::Normal | WhiteSpaceMode::Nowrap | WhiteSpaceMode::PreLine
9241 );
9242 if strip_leading {
9243 while !cursor.is_done() {
9244 let next_unit = cursor.peek_next_unit();
9245 if next_unit.is_empty() {
9246 break;
9247 }
9248 if next_unit.len() == 1 && is_collapsible_whitespace(&next_unit[0]) {
9249 cursor.consume(1);
9250 } else {
9251 break;
9252 }
9253 }
9254 }
9255
9256 let no_wrap = matches!(white_space_mode, WhiteSpaceMode::Nowrap | WhiteSpaceMode::Pre);
9260
9261 if no_wrap {
9262 loop {
9265 let next_unit = cursor.peek_next_unit();
9266 if next_unit.is_empty() {
9267 break;
9268 }
9269 if let Some(ShapedItem::Break { .. }) = next_unit.first() {
9270 line_items.push(next_unit[0].clone());
9271 cursor.consume(1);
9272 return (line_items, false);
9273 }
9274 line_items.extend_from_slice(&next_unit);
9275 cursor.consume(next_unit.len());
9276 }
9277 } else {
9278
9279 loop {
9280 let next_unit = if line_break == LineBreakStrictness::Anywhere {
9282 cursor.peek_next_single_item()
9283 } else {
9284 cursor.peek_next_unit()
9285 };
9286 if next_unit.is_empty() {
9287 break; }
9289
9290 if let Some(ShapedItem::Break { .. }) = next_unit.first() {
9291 line_items.push(next_unit[0].clone());
9292 cursor.consume(1);
9293 return (line_items, false);
9294 }
9295
9296 if line_constraints.is_min_content
9304 && !line_items.is_empty()
9305 && next_unit.len() == 1
9306 && is_break_opportunity_with_word_break(&next_unit[0], cursor.word_break, cursor.hyphens)
9307 {
9308 break;
9309 }
9310
9311 let unit_width: f32 = next_unit
9312 .iter()
9313 .map(|item| get_item_measure_with_spacing(item, is_vertical))
9314 .sum();
9315 let available_width = line_constraints.total_available - current_width;
9316
9317 if unit_width <= available_width {
9319 line_items.extend_from_slice(&next_unit);
9320 current_width += unit_width;
9321 cursor.consume(next_unit.len());
9322 } else {
9323 if line_break != LineBreakStrictness::Anywhere {
9325 if let Some(hyphenator) = hyphenator {
9326 if !is_break_opportunity(next_unit.last().unwrap()) {
9327 if let Some(hyphenation_result) = try_hyphenate_word_cluster(
9328 &next_unit,
9329 available_width,
9330 is_vertical,
9331 hyphenator,
9332 fonts,
9333 ) {
9334 line_items.extend(hyphenation_result.line_part);
9335 cursor.consume(next_unit.len());
9336 cursor.partial_remainder = hyphenation_result.remainder_part;
9337 return (line_items, true);
9338 }
9339 }
9340 }
9341 }
9342
9343 if line_items.is_empty() {
9351 match overflow_wrap {
9352 OverflowWrap::Anywhere | OverflowWrap::BreakWord => {
9353 let avail = line_constraints.total_available;
9360 for item in &next_unit {
9361 let item_w = get_item_measure_with_spacing(item, is_vertical);
9362 if !line_items.is_empty() && avail > 0.0 && current_width + item_w > avail {
9366 break;
9367 }
9368 line_items.push(item.clone());
9369 current_width += item_w;
9370 }
9377 let consumed = line_items.len().max(1);
9378 if line_items.is_empty() {
9379 line_items.push(next_unit[0].clone());
9380 }
9381 cursor.consume(consumed);
9382 }
9383 OverflowWrap::Normal => {
9384 line_items.extend_from_slice(&next_unit);
9388 cursor.consume(next_unit.len());
9389 }
9390 }
9391 }
9392 break;
9393 }
9394 }
9395
9396 } let strip_trailing = matches!(
9406 white_space_mode,
9407 WhiteSpaceMode::Normal | WhiteSpaceMode::Nowrap | WhiteSpaceMode::PreLine
9408 );
9409 if strip_trailing {
9410 while let Some(last) = line_items.last() {
9411 if is_collapsible_whitespace(last) {
9412 line_items.pop();
9413 } else {
9414 break;
9415 }
9416 }
9417 }
9418
9419 (line_items, false)
9420}
9421
9422#[derive(Debug, Clone)]
9424pub struct HyphenationBreak {
9425 pub char_len_on_line: usize,
9427 pub width_on_line: f32,
9429 pub line_part: Vec<ShapedItem>,
9431 pub hyphen_item: ShapedItem,
9433 pub remainder_part: Vec<ShapedItem>,
9436}
9437
9438#[allow(clippy::cast_precision_loss)] #[must_use] pub fn find_all_hyphenation_breaks<T: ParsedFontTrait>(
9444 word_clusters: &[ShapedCluster],
9445 hyphenator: &Standard,
9446 is_vertical: bool, fonts: &LoadedFonts<T>,
9448) -> Option<Vec<HyphenationBreak>> {
9449 if word_clusters.is_empty() {
9450 return None;
9451 }
9452
9453 let mut word_string = String::new();
9455 let mut char_map = Vec::new();
9456 let mut current_width = 0.0;
9457
9458 for (cluster_idx, cluster) in word_clusters.iter().enumerate() {
9459 for (char_byte_offset, _ch) in cluster.text.char_indices() {
9460 let glyph_idx = cluster
9461 .glyphs
9462 .iter()
9463 .rposition(|g| g.cluster_offset as usize <= char_byte_offset)
9464 .unwrap_or(0);
9465 let glyph = &cluster.glyphs[glyph_idx];
9466
9467 let num_chars_in_glyph = cluster.text[glyph.cluster_offset as usize..]
9468 .chars()
9469 .count();
9470 let advance_per_char = if is_vertical {
9471 glyph.vertical_advance
9472 } else {
9473 glyph.advance
9474 } / (num_chars_in_glyph as f32).max(1.0);
9475
9476 current_width += advance_per_char;
9477 char_map.push((cluster_idx, glyph_idx, current_width));
9478 }
9479 word_string.push_str(&cluster.text);
9480 }
9481
9482 let opportunities = hyphenator.hyphenate(&word_string);
9485 if opportunities.breaks.is_empty() {
9486 return None;
9487 }
9488
9489 let last_cluster = word_clusters.last().unwrap();
9490 let last_glyph = last_cluster.glyphs.last().unwrap();
9491 let style = last_cluster.style.clone();
9492
9493 let font = fonts.get_by_hash(last_glyph.font_hash)?;
9495 let (hyphen_glyph_id, hyphen_advance) =
9496 font.get_hyphen_glyph_and_advance(style.font_size_px)?;
9497
9498 let mut possible_breaks = Vec::new();
9499
9500 for &break_char_idx in &opportunities.breaks {
9502 if break_char_idx == 0 || break_char_idx > char_map.len() {
9505 continue;
9506 }
9507
9508 let (_, _, width_at_break) = char_map[break_char_idx - 1];
9509
9510 let line_part: Vec<ShapedItem> = word_clusters[..break_char_idx]
9512 .iter()
9513 .map(|c| ShapedItem::Cluster(c.clone()))
9514 .collect();
9515
9516 let remainder_part: Vec<ShapedItem> = word_clusters[break_char_idx..]
9518 .iter()
9519 .map(|c| ShapedItem::Cluster(c.clone()))
9520 .collect();
9521
9522 let hyphen_item = ShapedItem::Cluster(ShapedCluster {
9523 text: "-".to_string(),
9524 source_cluster_id: GraphemeClusterId {
9525 source_run: u32::MAX,
9526 start_byte_in_run: u32::MAX,
9527 },
9528 source_content_index: ContentIndex {
9529 run_index: u32::MAX,
9530 item_index: u32::MAX,
9531 },
9532 source_node_id: None, glyphs: smallvec![ShapedGlyph {
9534 kind: GlyphKind::Hyphen,
9535 glyph_id: hyphen_glyph_id,
9536 font_hash: last_glyph.font_hash,
9537 font_metrics: last_glyph.font_metrics,
9538 cluster_offset: 0,
9539 script: Script::Latin,
9540 advance: hyphen_advance,
9541 kerning: 0.0,
9542 offset: Point::default(),
9543 style: style.clone(),
9544 vertical_advance: hyphen_advance,
9545 vertical_offset: Point::default(),
9546 }],
9547 advance: hyphen_advance,
9548 direction: BidiDirection::Ltr,
9549 style: style.clone(),
9550 marker_position_outside: None,
9551 is_first_fragment: true,
9552 is_last_fragment: true,
9553 });
9554
9555 possible_breaks.push(HyphenationBreak {
9556 char_len_on_line: break_char_idx,
9557 width_on_line: width_at_break + hyphen_advance,
9558 line_part,
9559 hyphen_item,
9560 remainder_part,
9561 });
9562 }
9563
9564 Some(possible_breaks)
9565}
9566
9567fn try_hyphenate_word_cluster<T: ParsedFontTrait>(
9569 word_items: &[ShapedItem],
9570 remaining_width: f32,
9571 is_vertical: bool,
9572 hyphenator: &Standard,
9573 fonts: &LoadedFonts<T>,
9574) -> Option<HyphenationResult> {
9575 let word_clusters: Vec<ShapedCluster> = word_items
9576 .iter()
9577 .filter_map(|item| item.as_cluster().cloned())
9578 .collect();
9579
9580 if word_clusters.is_empty() {
9581 return None;
9582 }
9583
9584 let all_breaks = find_all_hyphenation_breaks(&word_clusters, hyphenator, is_vertical, fonts)?;
9585
9586 if let Some(best_break) = all_breaks
9587 .into_iter()
9588 .rfind(|b| b.width_on_line <= remaining_width)
9589 {
9590 let mut line_part = best_break.line_part;
9591 line_part.push(best_break.hyphen_item);
9592
9593 return Some(HyphenationResult {
9594 line_part,
9595 remainder_part: best_break.remainder_part,
9596 });
9597 }
9598
9599 None
9600}
9601
9602#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_precision_loss)] #[allow(clippy::match_same_arms)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn position_one_line<T: ParsedFontTrait>(
9680 line_items: &[ShapedItem],
9681 line_constraints: &LineConstraints,
9682 line_top_y: f32,
9683 line_index: usize,
9684 text_align: TextAlign,
9685 base_direction: BidiDirection,
9686 is_last_line: bool,
9687 constraints: &UnifiedConstraints,
9688 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
9689 fonts: &LoadedFonts<T>,
9690 is_after_forced_break: bool,
9691) -> (Vec<PositionedItem>, f32) {
9692 let line_text: String = line_items
9693 .iter()
9694 .filter_map(|i| i.as_cluster())
9695 .map(|c| c.text.as_str())
9696 .collect();
9697 if let Some(msgs) = debug_messages {
9698 msgs.push(LayoutDebugMessage::info(format!(
9699 "\n--- Entering position_one_line for line: [{line_text}] ---"
9700 )));
9701 }
9702 let physical_align = match (text_align, base_direction) {
9706 (TextAlign::Start, BidiDirection::Ltr) => TextAlign::Left,
9707 (TextAlign::Start, BidiDirection::Rtl) => TextAlign::Right,
9708 (TextAlign::End, BidiDirection::Ltr) => TextAlign::Right,
9709 (TextAlign::End, BidiDirection::Rtl) => TextAlign::Left,
9710 (other, _) => other,
9712 };
9713 if let Some(msgs) = debug_messages {
9714 msgs.push(LayoutDebugMessage::info(format!(
9715 "[Pos1Line] Physical align: {physical_align:?}"
9716 )));
9717 }
9718
9719 if line_items.is_empty() {
9723 return (Vec::new(), 0.0);
9724 }
9725 let mut positioned = Vec::new();
9726 let is_vertical = constraints.is_vertical();
9727
9728 let (content_ascent, content_descent) = calculate_line_metrics(line_items, constraints.vertical_align, constraints);
9733
9734 let strut_ad = constraints.strut_ascent + constraints.strut_descent;
9741 let strut_leading_half = (constraints.resolved_line_height() - strut_ad) / 2.0;
9742 let strut_above = constraints.strut_ascent + strut_leading_half;
9743 let strut_below = constraints.strut_descent + strut_leading_half;
9744 let line_ascent = content_ascent.max(strut_above);
9745 let line_descent = content_descent.max(strut_below);
9746 let line_box_height = line_ascent + line_descent;
9747
9748 let line_baseline_y = line_top_y + line_ascent;
9750
9751 let mut item_cursor = 0;
9753 let is_first_line_of_para = line_index == 0; for (segment_idx, segment) in line_constraints.segments.iter().enumerate() {
9756 if item_cursor >= line_items.len() {
9757 break;
9758 }
9759
9760 let mut segment_items = Vec::new();
9762 let mut current_segment_width = 0.0;
9763 while item_cursor < line_items.len() {
9764 let item = &line_items[item_cursor];
9765 let item_measure = get_item_measure(item, is_vertical);
9766 if current_segment_width + item_measure > segment.width && !segment_items.is_empty() {
9768 break;
9769 }
9770 segment_items.push(item.clone());
9771 current_segment_width += item_measure;
9772 item_cursor += 1;
9773 }
9774
9775 if segment_items.is_empty() {
9776 continue;
9777 }
9778
9779 let (extra_word_spacing, extra_char_spacing) = if (constraints.text_align == TextAlign::Justify
9786 || constraints.text_align == TextAlign::JustifyAll)
9787 && constraints.text_justify != JustifyContent::None
9788 && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
9789 && constraints.text_justify != JustifyContent::Kashida
9790 {
9791 let segment_line_constraints = LineConstraints {
9792 segments: vec![*segment],
9793 total_available: segment.width,
9794 is_min_content: false,
9795 };
9796 calculate_justification_spacing(
9797 &segment_items,
9798 &segment_line_constraints,
9799 constraints.text_justify,
9800 is_vertical,
9801 )
9802 } else {
9803 (0.0, 0.0)
9804 };
9805
9806 let justified_segment_items = if constraints.text_justify == JustifyContent::Kashida
9808 && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
9809 {
9810 let segment_line_constraints = LineConstraints {
9811 segments: vec![*segment],
9812 total_available: segment.width,
9813 is_min_content: false,
9814 };
9815 justify_kashida_and_rebuild(
9816 segment_items,
9817 &segment_line_constraints,
9818 is_vertical,
9819 debug_messages,
9820 fonts,
9821 )
9822 } else {
9823 segment_items
9824 };
9825
9826 let final_segment_width: f32 = justified_segment_items
9828 .iter()
9829 .map(|item| get_item_measure(item, is_vertical))
9830 .sum();
9831
9832 let trailing_ws_width = match constraints.white_space_mode {
9844 WhiteSpaceMode::BreakSpaces | WhiteSpaceMode::Pre => 0.0,
9845 WhiteSpaceMode::Normal | WhiteSpaceMode::Nowrap | WhiteSpaceMode::PreLine => {
9846 measure_trailing_whitespace(&justified_segment_items, is_vertical)
9847 }
9848 WhiteSpaceMode::PreWrap => {
9850 let has_forced_break = justified_segment_items.last()
9851 .is_some_and(|item| matches!(item, ShapedItem::Break { .. }));
9852 let ws_width = measure_trailing_whitespace(&justified_segment_items, is_vertical);
9853 if has_forced_break {
9854 let content_width = final_segment_width - ws_width;
9857 if content_width + ws_width > segment.width {
9858 ws_width
9859 } else {
9860 0.0
9861 }
9862 } else {
9863 ws_width }
9865 }
9866 };
9867 let effective_segment_width = final_segment_width - trailing_ws_width;
9868
9869 let remaining_space = segment.width - effective_segment_width;
9872
9873 let is_indefinite_width = segment.width.is_infinite() || segment.width > 1e30;
9879 let alignment_offset = if is_indefinite_width {
9881 0.0 } else {
9883 match physical_align {
9884 TextAlign::Center => remaining_space / 2.0,
9885 TextAlign::Right => remaining_space,
9886 TextAlign::Justify | TextAlign::JustifyAll
9887 if remaining_space > 0.0
9888 && extra_word_spacing == 0.0
9889 && extra_char_spacing == 0.0 =>
9890 {
9891 remaining_space / 2.0
9894 }
9895 _ => 0.0, }
9897 };
9898
9899 let mut main_axis_pen = segment.start_x + alignment_offset;
9900 if let Some(msgs) = debug_messages {
9901 msgs.push(LayoutDebugMessage::info(format!(
9902 "[Pos1Line] Segment width: {}, Item width: {}, Remaining space: {}, Initial pen: \
9903 {}",
9904 segment.width, final_segment_width, remaining_space, main_axis_pen
9905 )));
9906 }
9907
9908 if segment_idx == 0 {
9911 let is_indent_target = if constraints.text_indent_each_line {
9912 is_first_line_of_para || is_after_forced_break
9914 } else {
9915 is_first_line_of_para
9917 };
9918 let should_indent = if constraints.text_indent_hanging {
9920 !is_indent_target
9921 } else {
9922 is_indent_target
9923 };
9924 if should_indent {
9925 main_axis_pen += constraints.text_indent;
9926 }
9927 }
9928
9929 let total_marker_width: f32 = justified_segment_items
9932 .iter()
9933 .filter_map(|item| {
9934 if let ShapedItem::Cluster(c) = item {
9935 if c.marker_position_outside == Some(true) {
9936 return Some(get_item_measure(item, is_vertical));
9937 }
9938 }
9939 None
9940 })
9941 .sum();
9942
9943 let marker_spacing = 4.0; let mut marker_pen = if total_marker_width > 0.0 {
9946 -(total_marker_width + marker_spacing)
9947 } else {
9948 0.0
9949 };
9950
9951 let inline_offsets: Vec<(f32, f32)> = {
9977 let items_slice: &[ShapedItem] = &justified_segment_items;
9978 items_slice.iter().enumerate().map(|(idx, item)| {
9979 if let ShapedItem::Cluster(c) = item {
9980 if let Some(border) = c.style.border.as_ref() {
9981 if border.has_chrome() {
9982 let style_ptr = Arc::as_ptr(&c.style);
9983 let prev_same_span = idx > 0 && items_slice[idx - 1]
9984 .as_cluster()
9985 .is_some_and(|pc| Arc::as_ptr(&pc.style) == style_ptr);
9986 let next_same_span = idx + 1 < items_slice.len() && items_slice[idx + 1]
9987 .as_cluster()
9988 .is_some_and(|nc| Arc::as_ptr(&nc.style) == style_ptr);
9989 let left = if prev_same_span { 0.0 } else { border.left_inset() };
9990 let right = if next_same_span { 0.0 } else { border.right_inset() };
9991 return (left, right);
9992 }
9993 }
9994 }
9995 (0.0, 0.0)
9996 }).collect()
9997 };
9998 for (inline_offset_idx, item) in justified_segment_items.into_iter().enumerate() {
9999 let (item_ascent, item_descent) = get_item_vertical_metrics(&item, constraints);
10000 let effective_align = get_item_vertical_align(&item)
10002 .unwrap_or(constraints.vertical_align);
10003 let item_baseline_pos = match effective_align {
10007 VerticalAlign::Top => line_top_y + item_ascent,
10011 VerticalAlign::Middle => {
10013 let half_x_height = constraints.strut_x_height / 2.0;
10014 line_baseline_y + half_x_height - f32::midpoint(item_ascent, item_descent) + item_ascent
10015 }
10016 VerticalAlign::Bottom => line_top_y + line_box_height - item_descent,
10018 VerticalAlign::Sub => line_baseline_y + line_ascent * SUBSCRIPT_OFFSET_RATIO,
10020 VerticalAlign::Super => line_baseline_y - line_ascent * SUPERSCRIPT_OFFSET_RATIO,
10023 VerticalAlign::TextTop => (line_baseline_y - constraints.strut_ascent) + item_ascent,
10026 VerticalAlign::TextBottom => (line_baseline_y + constraints.strut_descent) - item_descent,
10029 VerticalAlign::Offset(offset) => line_baseline_y - offset,
10031 VerticalAlign::Baseline => line_baseline_y,
10035 };
10036
10037 let item_measure = get_item_measure(&item, is_vertical);
10039
10040 let (left_inset, right_inset) = if inline_offset_idx < inline_offsets.len() {
10042 inline_offsets[inline_offset_idx]
10043 } else {
10044 (0.0, 0.0)
10045 };
10046 main_axis_pen += left_inset;
10047
10048 let position = if is_vertical {
10049 Point {
10050 x: item_baseline_pos - item_ascent,
10051 y: main_axis_pen,
10052 }
10053 } else {
10054 if let Some(msgs) = debug_messages {
10055 msgs.push(LayoutDebugMessage::info(format!(
10056 "[Pos1Line] is_vertical=false, main_axis_pen={main_axis_pen}, item_baseline_pos={item_baseline_pos}, \
10057 item_ascent={item_ascent}"
10058 )));
10059 }
10060
10061 let x_position = if let ShapedItem::Cluster(cluster) = &item {
10063 if cluster.marker_position_outside == Some(true) {
10064 let marker_width = item_measure;
10066 if let Some(msgs) = debug_messages {
10067 msgs.push(LayoutDebugMessage::info(format!(
10068 "[Pos1Line] Outside marker detected! width={marker_width}, positioning at \
10069 marker_pen={marker_pen}"
10070 )));
10071 }
10072 let pos = marker_pen;
10073 marker_pen += marker_width; pos
10075 } else {
10076 main_axis_pen
10077 }
10078 } else {
10079 main_axis_pen
10080 };
10081
10082 Point {
10083 y: item_baseline_pos - item_ascent,
10084 x: x_position,
10085 }
10086 };
10087
10088 let item_text = item
10090 .as_cluster()
10091 .map_or("[OBJ]", |c| c.text.as_str());
10092 if let Some(msgs) = debug_messages {
10093 msgs.push(LayoutDebugMessage::info(format!(
10094 "[Pos1Line] Positioning item '{item_text}' at pen_x={main_axis_pen}"
10095 )));
10096 }
10097 positioned.push(PositionedItem {
10098 item: item.clone(),
10099 position,
10100 line_index,
10101 });
10102
10103 let is_outside_marker = if let ShapedItem::Cluster(c) = &item {
10105 c.marker_position_outside == Some(true)
10106 } else {
10107 false
10108 };
10109
10110 if !is_outside_marker {
10111 main_axis_pen += item_measure;
10112 main_axis_pen += right_inset;
10114 }
10115
10116 let is_cursive = if let ShapedItem::Cluster(c) = &item { is_cursive_script_cluster(c) } else { false };
10119 if !is_outside_marker && extra_char_spacing > 0.0 && can_justify_after(&item) && !is_cursive {
10120 main_axis_pen += extra_char_spacing;
10121 }
10122 if let ShapedItem::Cluster(c) = &item {
10126 if !is_outside_marker {
10127 if !is_cursive_script_cluster(c) {
10136 let letter_spacing_px = c.style.letter_spacing.resolve_px(c.style.font_size_px);
10137 main_axis_pen += letter_spacing_px;
10138 }
10139 if is_word_separator(&item) {
10141 let word_spacing_px = c.style.word_spacing.resolve_px(c.style.font_size_px);
10142 main_axis_pen += word_spacing_px;
10143 main_axis_pen += extra_word_spacing;
10144 }
10145 }
10146 }
10147 }
10148 }
10149
10150 (positioned, line_box_height)
10151}
10152
10153fn calculate_alignment_offset(
10155 items: &[ShapedItem],
10156 line_constraints: &LineConstraints,
10157 align: TextAlign,
10158 is_vertical: bool,
10159 constraints: &UnifiedConstraints,
10160) -> f32 {
10161 if let Some(segment) = line_constraints.segments.first() {
10163 let total_width: f32 = items
10166 .iter()
10167 .map(|item| get_item_measure_with_spacing(item, is_vertical))
10168 .sum();
10169
10170 let available_width = if constraints.segment_alignment == SegmentAlignment::Total {
10171 line_constraints.total_available
10172 } else {
10173 segment.width
10174 };
10175
10176 if total_width >= available_width {
10177 return 0.0; }
10179
10180 let remaining_space = available_width - total_width;
10181
10182 match align {
10183 TextAlign::Center => remaining_space / 2.0,
10184 TextAlign::Right => remaining_space,
10185 _ => 0.0, }
10187 } else {
10188 0.0
10189 }
10190}
10191
10192#[allow(clippy::cast_precision_loss)] fn calculate_justification_spacing(
10211 items: &[ShapedItem],
10212 line_constraints: &LineConstraints,
10213 text_justify: JustifyContent,
10214 is_vertical: bool,
10215) -> (f32, f32) {
10216 let total_width: f32 = items
10218 .iter()
10219 .map(|item| get_item_measure(item, is_vertical))
10220 .sum();
10221 let available_width = line_constraints.total_available;
10222
10223 if total_width >= available_width || available_width <= 0.0 {
10224 return (0.0, 0.0);
10225 }
10226
10227 let extra_space = available_width - total_width;
10228
10229 match text_justify {
10231 JustifyContent::InterWord => {
10232 let space_count = items.iter().filter(|item| is_word_separator(item)).count();
10234 if space_count > 0 {
10235 (extra_space / space_count as f32, 0.0)
10236 } else {
10237 (0.0, 0.0) }
10239 }
10240 JustifyContent::InterCharacter | JustifyContent::Distribute => {
10241 let gap_count = items
10243 .iter()
10244 .enumerate()
10245 .filter(|(i, item)| *i < items.len() - 1 && can_justify_after(item))
10246 .count();
10247 if gap_count > 0 {
10248 (0.0, extra_space / gap_count as f32)
10249 } else {
10250 (0.0, 0.0) }
10252 }
10253 _ => (0.0, 0.0),
10255 }
10256}
10257
10258#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] pub fn justify_kashida_and_rebuild<T: ParsedFontTrait>(
10266 items: Vec<ShapedItem>,
10267 line_constraints: &LineConstraints,
10268 is_vertical: bool,
10269 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
10270 fonts: &LoadedFonts<T>,
10271) -> Vec<ShapedItem> {
10272 if let Some(msgs) = debug_messages {
10273 msgs.push(LayoutDebugMessage::info(
10274 "\n--- Entering justify_kashida_and_rebuild ---".to_string(),
10275 ));
10276 }
10277 let total_width: f32 = items
10278 .iter()
10279 .map(|item| get_item_measure(item, is_vertical))
10280 .sum();
10281 let available_width = line_constraints.total_available;
10282 if let Some(msgs) = debug_messages {
10283 msgs.push(LayoutDebugMessage::info(format!(
10284 "Total item width: {total_width}, Available width: {available_width}"
10285 )));
10286 }
10287
10288 if total_width >= available_width || available_width <= 0.0 {
10289 if let Some(msgs) = debug_messages {
10290 msgs.push(LayoutDebugMessage::info(
10291 "No justification needed (line is full or invalid).".to_string(),
10292 ));
10293 }
10294 return items;
10295 }
10296
10297 let extra_space = available_width - total_width;
10298 if let Some(msgs) = debug_messages {
10299 msgs.push(LayoutDebugMessage::info(format!(
10300 "Extra space to fill: {extra_space}"
10301 )));
10302 }
10303
10304 let font_info = items.iter().find_map(|item| {
10305 if let ShapedItem::Cluster(c) = item {
10306 if let Some(glyph) = c.glyphs.first() {
10307 if glyph.script == Script::Arabic {
10308 if let Some(font) = fonts.get_by_hash(glyph.font_hash) {
10310 return Some((
10311 font.clone(),
10312 glyph.font_hash,
10313 glyph.font_metrics,
10314 glyph.style.clone(),
10315 ));
10316 }
10317 }
10318 }
10319 }
10320 None
10321 });
10322
10323 let (font, font_hash, font_metrics, style) = if let Some(info) = font_info {
10324 if let Some(msgs) = debug_messages {
10325 msgs.push(LayoutDebugMessage::info(
10326 "Found Arabic font for kashida.".to_string(),
10327 ));
10328 }
10329 info
10330 } else {
10331 if let Some(msgs) = debug_messages {
10332 msgs.push(LayoutDebugMessage::info(
10333 "No Arabic font found on line. Cannot insert kashidas.".to_string(),
10334 ));
10335 }
10336 return items;
10337 };
10338
10339 let (kashida_glyph_id, kashida_advance) =
10340 match font.get_kashida_glyph_and_advance(style.font_size_px) {
10341 Some((id, adv)) if adv > 0.0 => {
10342 if let Some(msgs) = debug_messages {
10343 msgs.push(LayoutDebugMessage::info(format!(
10344 "Font provides kashida glyph with advance {adv}"
10345 )));
10346 }
10347 (id, adv)
10348 }
10349 _ => {
10350 if let Some(msgs) = debug_messages {
10351 msgs.push(LayoutDebugMessage::info(
10352 "Font does not support kashida justification.".to_string(),
10353 ));
10354 }
10355 return items;
10356 }
10357 };
10358
10359 let opportunity_indices: Vec<usize> = items
10360 .windows(2)
10361 .enumerate()
10362 .filter_map(|(i, window)| {
10363 if let (ShapedItem::Cluster(cur), ShapedItem::Cluster(next)) = (&window[0], &window[1])
10364 {
10365 if is_arabic_cluster(cur)
10366 && is_arabic_cluster(next)
10367 && !is_word_separator(&window[1])
10368 {
10369 return Some(i + 1);
10370 }
10371 }
10372 None
10373 })
10374 .collect();
10375
10376 if let Some(msgs) = debug_messages {
10377 msgs.push(LayoutDebugMessage::info(format!(
10378 "Found {} kashida insertion opportunities at indices: {:?}",
10379 opportunity_indices.len(),
10380 opportunity_indices
10381 )));
10382 }
10383
10384 if opportunity_indices.is_empty() {
10385 if let Some(msgs) = debug_messages {
10386 msgs.push(LayoutDebugMessage::info(
10387 "No opportunities found. Exiting.".to_string(),
10388 ));
10389 }
10390 return items;
10391 }
10392
10393 let num_kashidas_to_insert = (extra_space / kashida_advance).floor() as usize;
10394 if let Some(msgs) = debug_messages {
10395 msgs.push(LayoutDebugMessage::info(format!(
10396 "Calculated number of kashidas to insert: {num_kashidas_to_insert}"
10397 )));
10398 }
10399
10400 if num_kashidas_to_insert == 0 {
10401 return items;
10402 }
10403
10404 let kashidas_per_point = num_kashidas_to_insert / opportunity_indices.len();
10405 let mut remainder = num_kashidas_to_insert % opportunity_indices.len();
10406 if let Some(msgs) = debug_messages {
10407 msgs.push(LayoutDebugMessage::info(format!(
10408 "Distributing kashidas: {kashidas_per_point} per point, with {remainder} remainder."
10409 )));
10410 }
10411
10412 let kashida_item = {
10413 let kashida_glyph = ShapedGlyph {
10415 kind: GlyphKind::Kashida {
10416 width: kashida_advance,
10417 },
10418 glyph_id: kashida_glyph_id,
10419 font_hash,
10420 font_metrics,
10421 style: style.clone(),
10422 script: Script::Arabic,
10423 advance: kashida_advance,
10424 kerning: 0.0,
10425 cluster_offset: 0,
10426 offset: Point::default(),
10427 vertical_advance: 0.0,
10428 vertical_offset: Point::default(),
10429 };
10430 ShapedItem::Cluster(ShapedCluster {
10431 text: "\u{0640}".to_string(),
10432 source_cluster_id: GraphemeClusterId {
10433 source_run: u32::MAX,
10434 start_byte_in_run: u32::MAX,
10435 },
10436 source_content_index: ContentIndex {
10437 run_index: u32::MAX,
10438 item_index: u32::MAX,
10439 },
10440 source_node_id: None, glyphs: smallvec![kashida_glyph],
10442 advance: kashida_advance,
10443 direction: BidiDirection::Ltr,
10444 style,
10445 marker_position_outside: None,
10446 is_first_fragment: true,
10447 is_last_fragment: true,
10448 })
10449 };
10450
10451 let mut new_items = Vec::with_capacity(items.len() + num_kashidas_to_insert);
10452 let mut last_copy_idx = 0;
10453 for &point in &opportunity_indices {
10454 new_items.extend_from_slice(&items[last_copy_idx..point]);
10455 let mut num_to_insert = kashidas_per_point;
10456 if remainder > 0 {
10457 num_to_insert += 1;
10458 remainder -= 1;
10459 }
10460 for _ in 0..num_to_insert {
10461 new_items.push(kashida_item.clone());
10462 }
10463 last_copy_idx = point;
10464 }
10465 new_items.extend_from_slice(&items[last_copy_idx..]);
10466
10467 if let Some(msgs) = debug_messages {
10468 msgs.push(LayoutDebugMessage::info(format!(
10469 "--- Exiting justify_kashida_and_rebuild, new item count: {} ---",
10470 new_items.len()
10471 )));
10472 }
10473 new_items
10474}
10475
10476fn is_arabic_cluster(cluster: &ShapedCluster) -> bool {
10478 cluster.glyphs.iter().any(|g| g.script == Script::Arabic)
10481}
10482
10483fn measure_trailing_whitespace(items: &[ShapedItem], is_vertical: bool) -> f32 {
10485 let mut trailing_ws = 0.0;
10486 for item in items.iter().rev() {
10487 if is_collapsible_whitespace(item) {
10488 trailing_ws += get_item_measure(item, is_vertical);
10489 } else {
10490 break;
10491 }
10492 }
10493 trailing_ws
10494}
10495
10496#[must_use] pub fn is_collapsible_whitespace(item: &ShapedItem) -> bool {
10501 if let ShapedItem::Cluster(c) = item {
10502 c.text.chars().all(|ch| matches!(ch,
10503 ' ' | '\t' | '\u{1680}' ))
10505 } else {
10506 false
10507 }
10508}
10509
10510pub fn is_cursive_script_cluster(c: &ShapedCluster) -> bool {
10517 c.text.chars().next().is_some_and(is_cursive_script_char)
10518}
10519
10520fn is_cursive_script_char(ch: char) -> bool {
10521 let cp = ch as u32;
10522 if (0x0600..=0x06FF).contains(&cp) { return true; }
10524 if (0x0750..=0x077F).contains(&cp) { return true; }
10525 if (0x08A0..=0x08FF).contains(&cp) { return true; }
10526 if (0xFB50..=0xFDFF).contains(&cp) { return true; }
10527 if (0xFE70..=0xFEFF).contains(&cp) { return true; }
10528 if (0x0700..=0x074F).contains(&cp) { return true; }
10530 if (0x1800..=0x18AF).contains(&cp) { return true; }
10532 if (0x07C0..=0x07FF).contains(&cp) { return true; }
10534 if (0x0840..=0x085F).contains(&cp) { return true; }
10536 if (0xA840..=0xA87F).contains(&cp) { return true; }
10538 if (0x10D00..=0x10D3F).contains(&cp) { return true; }
10540 false
10541}
10542
10543pub(crate) fn is_word_char(ch: char) -> bool {
10552 ch.is_alphanumeric() || ch == '_'
10553}
10554
10555fn cluster_is_word_boundary(cluster: &ShapedCluster) -> bool {
10559 !cluster.text.chars().any(is_word_char)
10560}
10561
10562pub fn is_word_separator(item: &ShapedItem) -> bool {
10564 if let ShapedItem::Cluster(c) = item {
10565 c.text.chars().any(is_word_separator_char)
10566 } else {
10567 false
10568 }
10569}
10570
10571#[must_use] pub fn is_no_break_space(item: &ShapedItem) -> bool {
10576 if let ShapedItem::Cluster(c) = item {
10577 c.text
10578 .chars()
10579 .any(|ch| matches!(ch, '\u{00A0}' | '\u{202F}' | '\u{2060}' | '\u{FEFF}'))
10580 } else {
10581 false
10582 }
10583}
10584
10585#[allow(clippy::match_same_arms)] const fn is_word_separator_char(c: char) -> bool {
10592 match c {
10593 '\u{0020}' => true,
10595 '\u{00A0}' => true,
10597 '\u{1680}' => true,
10599 '\u{1361}' => true,
10601 '\u{2000}'..='\u{200A}' => false,
10603 '\u{202F}' => true,
10605 '\u{205F}' => true,
10607 '\u{3000}' => false,
10609 '\u{10100}' => true,
10611 '\u{10101}' => true,
10613 '\u{1039F}' => true,
10615 '\u{1091F}' => true,
10617 _ => false,
10619 }
10620}
10621
10622#[must_use] pub fn is_zero_width_space(item: &ShapedItem) -> bool {
10628 if let ShapedItem::Cluster(c) = item {
10629 c.text.contains('\u{200B}')
10630 } else {
10631 false
10632 }
10633}
10634
10635fn can_justify_after(item: &ShapedItem) -> bool {
10637 if let ShapedItem::Cluster(c) = item {
10638 c.text.chars().last().is_some_and(|g| {
10639 !g.is_whitespace() && classify_character(g as u32) != CharacterClass::Combining
10640 })
10641 } else {
10642 false
10646 }
10647}
10648
10649#[allow(clippy::match_same_arms)] const fn classify_character(codepoint: u32) -> CharacterClass {
10654 match codepoint {
10655 0x0020 | 0x00A0 | 0x3000 => CharacterClass::Space,
10656 0x0021..=0x002F | 0x003A..=0x0040 | 0x005B..=0x0060 | 0x007B..=0x007E => {
10657 CharacterClass::Punctuation
10658 }
10659 0x4E00..=0x9FFF | 0x3400..=0x4DBF => CharacterClass::Ideograph,
10660 0x0300..=0x036F | 0x1AB0..=0x1AFF => CharacterClass::Combining,
10661 0x1800..=0x18AF => CharacterClass::Letter,
10663 _ => CharacterClass::Letter,
10664 }
10665}
10666
10667#[must_use] pub fn get_item_measure(item: &ShapedItem, is_vertical: bool) -> f32 {
10669 match item {
10670 ShapedItem::Cluster(c) => {
10671 let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
10675 c.advance + total_kerning
10676 }
10677 ShapedItem::Object { bounds, .. }
10678 | ShapedItem::CombinedBlock { bounds, .. }
10679 | ShapedItem::Tab { bounds, .. } => {
10680 if is_vertical {
10681 bounds.height
10682 } else {
10683 bounds.width
10684 }
10685 }
10686 ShapedItem::Break { .. } => 0.0,
10687 }
10688}
10689
10690#[must_use]
10699pub fn get_item_measure_with_spacing(item: &ShapedItem, is_vertical: bool) -> f32 {
10700 let base = get_item_measure(item, is_vertical);
10701 if let ShapedItem::Cluster(c) = item {
10702 let mut extra = 0.0;
10703 if !is_cursive_script_cluster(c) {
10704 extra += c.style.letter_spacing.resolve_px(c.style.font_size_px);
10705 }
10706 if is_word_separator(item) {
10707 extra += c.style.word_spacing.resolve_px(c.style.font_size_px);
10708 }
10709 base + extra
10710 } else {
10711 base
10712 }
10713}
10714
10715#[allow(clippy::match_same_arms)] fn get_line_constraints(
10719 line_y: f32,
10720 line_height: f32,
10721 constraints: &UnifiedConstraints,
10722 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
10723) -> LineConstraints {
10724 if let Some(msgs) = debug_messages {
10725 msgs.push(LayoutDebugMessage::info(format!(
10726 "\n--- Entering get_line_constraints for y={line_y} ---"
10727 )));
10728 }
10729
10730 let mut available_segments = Vec::new();
10731 if constraints.shape_boundaries.is_empty() {
10732 let segment_width = match constraints.available_width {
10743 AvailableSpace::Definite(w) => w, AvailableSpace::MaxContent => f32::MAX / 2.0, AvailableSpace::MinContent => f32::MAX / 2.0, };
10747 available_segments.push(LineSegment {
10750 start_x: 0.0,
10751 width: segment_width,
10752 priority: 0,
10753 });
10754 } else {
10755 }
10757
10758 if let Some(msgs) = debug_messages {
10759 msgs.push(LayoutDebugMessage::info(format!(
10760 "Initial available segments: {available_segments:?}"
10761 )));
10762 }
10763
10764 for (idx, exclusion) in constraints.shape_exclusions.iter().enumerate() {
10765 if let Some(msgs) = debug_messages {
10766 msgs.push(LayoutDebugMessage::info(format!(
10767 "Applying exclusion #{idx}: {exclusion:?}"
10768 )));
10769 }
10770 let exclusion_spans =
10771 get_shape_horizontal_spans(exclusion, line_y, line_height);
10772 if let Some(msgs) = debug_messages {
10773 msgs.push(LayoutDebugMessage::info(format!(
10774 " Exclusion spans at y={line_y}: {exclusion_spans:?}"
10775 )));
10776 }
10777
10778 if exclusion_spans.is_empty() {
10779 continue;
10780 }
10781
10782 let mut next_segments = Vec::new();
10783 for (excl_start, excl_end) in exclusion_spans {
10784 for segment in &available_segments {
10785 let seg_start = segment.start_x;
10786 let seg_end = segment.start_x + segment.width;
10787
10788 if seg_end > excl_start && seg_start < excl_end {
10790 if seg_start < excl_start {
10791 next_segments.push(LineSegment {
10793 start_x: seg_start,
10794 width: excl_start - seg_start,
10795 priority: segment.priority,
10796 });
10797 }
10798 if seg_end > excl_end {
10799 next_segments.push(LineSegment {
10801 start_x: excl_end,
10802 width: seg_end - excl_end,
10803 priority: segment.priority,
10804 });
10805 }
10806 } else {
10807 next_segments.push(*segment); }
10809 }
10810 available_segments = merge_segments(next_segments);
10811 next_segments = Vec::new();
10812 }
10813 if let Some(msgs) = debug_messages {
10814 msgs.push(LayoutDebugMessage::info(format!(
10815 " Segments after exclusion #{idx}: {available_segments:?}"
10816 )));
10817 }
10818 }
10819
10820 let total_width = available_segments.iter().map(|s| s.width).sum();
10821 if let Some(msgs) = debug_messages {
10822 msgs.push(LayoutDebugMessage::info(format!(
10823 "Final segments: {available_segments:?}, total available width: {total_width}"
10824 )));
10825 msgs.push(LayoutDebugMessage::info(
10826 "--- Exiting get_line_constraints ---".to_string(),
10827 ));
10828 }
10829
10830 LineConstraints {
10831 segments: available_segments,
10832 total_available: total_width,
10833 is_min_content: matches!(constraints.available_width, AvailableSpace::MinContent),
10834 }
10835}
10836
10837#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss)]
10844fn flatten_svg_to_path_segments(
10845 multipolygon: &azul_core::svg::SvgMultiPolygon,
10846 reference_box: Rect,
10847) -> Vec<PathSegment> {
10848 use azul_core::svg::SvgPathElement;
10849
10850 let mut out: Vec<PathSegment> = Vec::new();
10851
10852 for ring in multipolygon.rings.as_ref() {
10853 let elements = ring.items.as_ref();
10854 if elements.is_empty() {
10855 continue;
10856 }
10857 let start = elements[0].get_start();
10858 out.push(PathSegment::MoveTo(Point {
10859 x: reference_box.x + start.x,
10860 y: reference_box.y + start.y,
10861 }));
10862 for el in elements {
10863 match el {
10864 SvgPathElement::Line(l) => {
10865 out.push(PathSegment::LineTo(Point {
10866 x: reference_box.x + l.end.x,
10867 y: reference_box.y + l.end.y,
10868 }));
10869 }
10870 curve => {
10871 let len = curve.get_length();
10873 let steps = ((len / 4.0).ceil() as usize).clamp(1, 64);
10874 for i in 1..=steps {
10875 let offset = len * (i as f64) / (steps as f64);
10876 let t = curve.get_t_at_offset(offset);
10877 out.push(PathSegment::LineTo(Point {
10878 x: reference_box.x + curve.get_x_at_t(t) as f32,
10879 y: reference_box.y + curve.get_y_at_t(t) as f32,
10880 }));
10881 }
10882 }
10883 }
10884 }
10885 out.push(PathSegment::Close);
10886 }
10887
10888 out
10889}
10890
10891fn path_segments_line_intersection(
10896 segments: &[PathSegment],
10897 y: f32,
10898 line_height: f32,
10899) -> Vec<(f32, f32)> {
10900 let line_center_y = y + line_height / 2.0;
10901 let mut crossings: Vec<f32> = Vec::new();
10902
10903 let mut subpath: Vec<Point> = Vec::new();
10906 let flush = |subpath: &mut Vec<Point>, crossings: &mut Vec<f32>| {
10907 if subpath.len() >= 2 {
10908 for i in 0..subpath.len() {
10909 let p1 = subpath[i];
10910 let p2 = subpath[(i + 1) % subpath.len()];
10911 if (p2.y - p1.y).abs() < f32::EPSILON {
10912 continue;
10913 }
10914 let crosses = (p1.y <= line_center_y && p2.y > line_center_y)
10915 || (p1.y > line_center_y && p2.y <= line_center_y);
10916 if crosses {
10917 let t = (line_center_y - p1.y) / (p2.y - p1.y);
10918 crossings.push(t.mul_add(p2.x - p1.x, p1.x));
10919 }
10920 }
10921 }
10922 subpath.clear();
10923 };
10924
10925 for seg in segments {
10926 match seg {
10927 PathSegment::MoveTo(p) => {
10928 flush(&mut subpath, &mut crossings);
10929 subpath.push(*p);
10930 }
10931 PathSegment::LineTo(p) => subpath.push(*p),
10932 PathSegment::Close => flush(&mut subpath, &mut crossings),
10933 PathSegment::CurveTo { end, .. } | PathSegment::QuadTo { end, .. } => {
10936 subpath.push(*end);
10937 }
10938 PathSegment::Arc { center, .. } => subpath.push(*center),
10939 }
10940 }
10941 flush(&mut subpath, &mut crossings);
10942
10943 crossings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
10944 let mut spans = Vec::new();
10945 for chunk in crossings.chunks_exact(2) {
10946 if chunk[1] > chunk[0] {
10947 spans.push((chunk[0], chunk[1]));
10948 }
10949 }
10950 spans
10951}
10952
10953#[allow(clippy::suboptimal_flops)] fn get_shape_horizontal_spans(
10957 shape: &ShapeBoundary,
10958 y: f32,
10959 line_height: f32,
10960) -> Vec<(f32, f32)> {
10961 match shape {
10962 ShapeBoundary::Rectangle(rect) => {
10963 let line_start = y;
10966 let line_end = y + line_height;
10967 let rect_start = rect.y;
10968 let rect_end = rect.y + rect.height;
10969
10970 if line_start < rect_end && line_end > rect_start {
10971 vec![(rect.x, rect.x + rect.width)]
10972 } else {
10973 vec![]
10974 }
10975 }
10976 ShapeBoundary::Circle { center, radius } => {
10977 let line_center_y = y + line_height / 2.0;
10978 let dy = (line_center_y - center.y).abs();
10979 if dy <= *radius {
10980 let dx = (radius.powi(2) - dy.powi(2)).sqrt();
10981 vec![(center.x - dx, center.x + dx)]
10982 } else {
10983 vec![]
10984 }
10985 }
10986 ShapeBoundary::Ellipse { center, radii } => {
10987 let line_center_y = y + line_height / 2.0;
10988 let dy = line_center_y - center.y;
10989 if dy.abs() <= radii.height {
10990 let y_term = dy / radii.height;
10992 let x_term_squared = 1.0 - y_term.powi(2);
10993 if x_term_squared >= 0.0 {
10994 let dx = radii.width * x_term_squared.sqrt();
10995 vec![(center.x - dx, center.x + dx)]
10996 } else {
10997 vec![]
10998 }
10999 } else {
11000 vec![]
11001 }
11002 }
11003 ShapeBoundary::Polygon { points } => {
11004 let segments = polygon_line_intersection(points, y, line_height);
11005 segments
11006 .iter()
11007 .map(|s| (s.start_x, s.start_x + s.width))
11008 .collect()
11009 }
11010 ShapeBoundary::Path { segments } => {
11015 path_segments_line_intersection(segments, y, line_height)
11016 }
11017 }
11018}
11019
11020fn merge_segments(mut segments: Vec<LineSegment>) -> Vec<LineSegment> {
11022 if segments.len() <= 1 {
11023 return segments;
11024 }
11025 segments.sort_by(|a, b| a.start_x.partial_cmp(&b.start_x).unwrap_or(Ordering::Equal));
11026 let mut merged = vec![segments[0]];
11027 for next_seg in segments.iter().skip(1) {
11028 let last = merged.last_mut().unwrap();
11029 if next_seg.start_x <= last.start_x + last.width {
11030 let new_width = (next_seg.start_x + next_seg.width) - last.start_x;
11031 last.width = last.width.max(new_width);
11032 } else {
11033 merged.push(*next_seg);
11034 }
11035 }
11036 merged
11037}
11038
11039#[allow(clippy::suboptimal_flops)] fn polygon_line_intersection(
11042 points: &[Point],
11043 y: f32,
11044 line_height: f32,
11045) -> Vec<LineSegment> {
11046 if points.len() < 3 {
11047 return vec![];
11048 }
11049
11050 let line_center_y = y + line_height / 2.0;
11051 let mut intersections = Vec::new();
11052
11053 for i in 0..points.len() {
11055 let p1 = points[i];
11056 let p2 = points[(i + 1) % points.len()];
11057
11058 if (p2.y - p1.y).abs() < f32::EPSILON {
11060 continue;
11061 }
11062
11063 let crosses = (p1.y <= line_center_y && p2.y > line_center_y)
11065 || (p1.y > line_center_y && p2.y <= line_center_y);
11066
11067 if crosses {
11068 let t = (line_center_y - p1.y) / (p2.y - p1.y);
11070 let x = p1.x + t * (p2.x - p1.x);
11071 intersections.push(x);
11072 }
11073 }
11074
11075 intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
11077
11078 let mut segments = Vec::new();
11080 for chunk in intersections.chunks_exact(2) {
11081 let start_x = chunk[0];
11082 let end_x = chunk[1];
11083 if end_x > start_x {
11084 segments.push(LineSegment {
11085 start_x,
11086 width: end_x - start_x,
11087 priority: 0,
11088 });
11089 }
11090 }
11091
11092 segments
11093}
11094
11095#[cfg(feature = "text_layout_hyphenation")]
11099fn get_hyphenator(language: HyphenationLanguage) -> Result<Standard, LayoutError> {
11100 Standard::from_embedded(language).map_err(|e| LayoutError::HyphenationError(e.to_string()))
11101}
11102
11103#[cfg(not(feature = "text_layout_hyphenation"))]
11105fn get_hyphenator(_language: Language) -> Result<Standard, LayoutError> {
11106 Err(LayoutError::HyphenationError("Hyphenation feature not enabled".to_string()))
11107}
11108
11109const fn is_break_suppressing_control(ch: char) -> bool {
11112 matches!(ch,
11113 '\u{200D}' | '\u{2060}' | '\u{FEFF}' )
11117}
11118
11119const fn is_break_forcing_control(ch: char) -> bool {
11120 matches!(ch,
11121 '\u{200B}' | '\u{2028}' | '\u{2029}' )
11125}
11126
11127const fn is_cjk_character(ch: char) -> bool {
11130 let cp = ch as u32;
11131 matches!(cp,
11132 0x4E00..=0x9FFF |
11134 0x3400..=0x4DBF |
11136 0x20000..=0x2A6DF |
11138 0xF900..=0xFAFF |
11140 0x3040..=0x309F |
11142 0x30A0..=0x30FF |
11144 0x31F0..=0x31FF |
11146 0x3000..=0x303F |
11148 0xFF00..=0xFFEF |
11150 0xAC00..=0xD7AF
11152 )
11153}
11154
11155fn is_cjk_cluster(cluster: &ShapedCluster) -> bool {
11157 cluster.text.chars().any(is_cjk_character)
11158}
11159
11160fn is_break_opportunity_with_word_break(item: &ShapedItem, word_break: WordBreak, hyphens: Hyphens) -> bool {
11173 if let ShapedItem::Cluster(c) = item {
11178 if c.text
11179 .chars()
11180 .any(|ch| matches!(ch, '\u{00A0}' | '\u{202F}' | '\u{2060}' | '\u{FEFF}'))
11181 {
11182 return false;
11183 }
11184 }
11185 if is_word_separator(item) {
11187 return true;
11188 }
11189 if let ShapedItem::Break { .. } = item {
11190 return true;
11191 }
11192 if is_zero_width_space(item) {
11198 return true;
11199 }
11200 if hyphens != Hyphens::None {
11202 if let ShapedItem::Cluster(c) = item {
11203 if c.text.starts_with('\u{00AD}') {
11204 return true;
11205 }
11206 }
11207 }
11208
11209 if let ShapedItem::Cluster(c) = item {
11216 if c.text.ends_with('\u{002D}') || c.text.ends_with('\u{2010}') || c.text.ends_with('\u{002F}') {
11217 return true;
11218 }
11219 }
11220
11221 match word_break {
11223 WordBreak::Normal => {
11224 if let ShapedItem::Cluster(c) = item {
11226 if is_cjk_cluster(c) {
11227 return true;
11228 }
11229 }
11230 false
11231 }
11232 WordBreak::BreakAll => {
11233 if let ShapedItem::Cluster(_) = item {
11235 return true;
11236 }
11237 false
11238 }
11239 WordBreak::KeepAll => {
11240 false
11243 }
11244 }
11245}
11246
11247#[allow(clippy::match_same_arms)] const fn is_cjk_break_allowed_by_strictness(
11258 ch: char,
11259 _prev_ch: Option<char>,
11260 strictness: LineBreakStrictness,
11261) -> bool {
11262 match strictness {
11263 LineBreakStrictness::Anywhere => true,
11264 LineBreakStrictness::Loose => {
11265 true
11268 }
11269 LineBreakStrictness::Normal | LineBreakStrictness::Auto => {
11270 match ch {
11274 '\u{2010}' | '\u{2013}' => false, _ => true,
11276 }
11277 }
11278 LineBreakStrictness::Strict => {
11279 match ch {
11284 '\u{301C}' | '\u{30A0}' => false, '\u{2010}' | '\u{2013}' => false, c if is_small_kana(c) => false,
11287 _ => true,
11288 }
11289 }
11290 }
11291}
11292
11293const fn is_small_kana(ch: char) -> bool {
11296 matches!(ch,
11297 '\u{3041}' | '\u{3043}' | '\u{3045}' | '\u{3047}' | '\u{3049}' | '\u{3063}' | '\u{3083}' | '\u{3085}' | '\u{3087}' | '\u{308E}' | '\u{3095}' | '\u{3096}' | '\u{30A1}' | '\u{30A3}' | '\u{30A5}' | '\u{30A7}' | '\u{30A9}' | '\u{30C3}' | '\u{30E3}' | '\u{30E5}' | '\u{30E7}' | '\u{30EE}' | '\u{30F5}' | '\u{30F6}' | '\u{30FC}' )
11323}
11324
11325fn is_break_opportunity(item: &ShapedItem) -> bool {
11328 if matches!(item, ShapedItem::Object { .. } | ShapedItem::CombinedBlock { .. }) {
11331 return true;
11332 }
11333 if let ShapedItem::Cluster(c) = item {
11337 if c.text.contains('\u{200B}') {
11339 return true;
11340 }
11341 if c.text.chars().any(is_break_forcing_control) {
11343 return true;
11344 }
11345 if c.text.chars().any(|ch| matches!(ch, '\u{2060}' | '\u{200D}' | '\u{00A0}')) {
11347 return false;
11348 }
11349 if c.text.ends_with('\u{002D}') || c.text.ends_with('\u{2010}') {
11353 return true;
11354 }
11355 }
11356 is_break_opportunity_with_word_break(item, WordBreak::Normal, Hyphens::Manual)
11357}
11358
11359#[derive(Debug, Clone)]
11364pub struct BreakCursor<'a> {
11365 pub items: &'a [ShapedItem],
11367 pub next_item_index: usize,
11369 pub partial_remainder: Vec<ShapedItem>,
11372 pub word_break: WordBreak,
11374 pub hyphens: Hyphens,
11375 pub line_break: LineBreakStrictness,
11376}
11377
11378impl<'a> BreakCursor<'a> {
11379 #[must_use] pub fn new(items: &'a [ShapedItem]) -> Self {
11380 Self {
11381 items,
11382 next_item_index: 0,
11383 partial_remainder: Vec::new(),
11384 word_break: WordBreak::Normal,
11385 hyphens: Hyphens::default(),
11386 line_break: LineBreakStrictness::default(),
11387 }
11388 }
11389
11390 #[must_use] pub fn with_word_break(items: &'a [ShapedItem], word_break: WordBreak) -> Self {
11391 Self {
11392 items,
11393 next_item_index: 0,
11394 partial_remainder: Vec::new(),
11395 word_break,
11396 hyphens: Hyphens::default(),
11397 line_break: LineBreakStrictness::default(),
11398 }
11399 }
11400
11401 #[must_use] pub const fn is_at_start(&self) -> bool {
11403 self.next_item_index == 0 && self.partial_remainder.is_empty()
11404 }
11405
11406 pub fn drain_remaining(&mut self) -> Vec<ShapedItem> {
11408 let mut remaining = std::mem::take(&mut self.partial_remainder);
11409 if self.next_item_index < self.items.len() {
11410 remaining.extend_from_slice(&self.items[self.next_item_index..]);
11411 }
11412 self.next_item_index = self.items.len();
11413 remaining
11414 }
11415
11416 #[must_use] pub const fn is_done(&self) -> bool {
11418 self.next_item_index >= self.items.len() && self.partial_remainder.is_empty()
11419 }
11420
11421 pub fn consume(&mut self, count: usize) {
11423 if count == 0 {
11424 return;
11425 }
11426
11427 let remainder_len = self.partial_remainder.len();
11428 if count <= remainder_len {
11429 self.partial_remainder.drain(..count);
11431 } else {
11432 let from_main_list = count - remainder_len;
11434 self.partial_remainder.clear();
11435 self.next_item_index += from_main_list;
11436 }
11437 }
11438
11439 pub fn peek_next_unit(&self) -> Vec<ShapedItem> {
11446 let mut unit = Vec::new();
11447 let mut source_items = self.partial_remainder.clone();
11448 source_items.extend_from_slice(&self.items[self.next_item_index..]);
11449
11450 if source_items.is_empty() {
11451 return unit;
11452 }
11453
11454 if is_break_opportunity_with_word_break(&source_items[0], self.word_break, self.hyphens) {
11456 unit.push(source_items[0].clone());
11457 return unit;
11458 }
11459
11460 let mut suppress_next_break = false;
11467 for (i, item) in source_items.iter().enumerate() {
11468 let starts_with_suppress = if let ShapedItem::Cluster(c) = item {
11471 c.text.chars().next().is_some_and(is_break_suppressing_control)
11472 } else {
11473 false
11474 };
11475 let cjk_strictness_suppressed = if let ShapedItem::Cluster(c) = item {
11477 c.text.chars().next().is_some_and(|ch| {
11478 !is_cjk_break_allowed_by_strictness(ch, None, self.line_break)
11479 })
11480 } else {
11481 false
11482 };
11483 if i > 0 && !suppress_next_break && !starts_with_suppress && !cjk_strictness_suppressed && is_break_opportunity_with_word_break(item, self.word_break, self.hyphens) {
11484 break;
11485 }
11486 suppress_next_break = false;
11487 unit.push(item.clone());
11488
11489 if let ShapedItem::Cluster(c) = item {
11491 if let Some(last_ch) = c.text.chars().last() {
11492 if is_break_suppressing_control(last_ch) {
11493 suppress_next_break = true;
11494 }
11495 }
11496 }
11497
11498 if self.word_break == WordBreak::BreakAll {
11500 if let ShapedItem::Cluster(_) = item {
11501 break;
11502 }
11503 }
11504 }
11505 unit
11506 }
11507
11508 #[must_use] pub fn peek_next_single_item(&self) -> Vec<ShapedItem> {
11509 if !self.partial_remainder.is_empty() {
11510 return vec![self.partial_remainder[0].clone()];
11511 }
11512 if self.next_item_index < self.items.len() {
11513 return vec![self.items[self.next_item_index].clone()];
11514 }
11515 Vec::new()
11516 }
11517}
11518
11519struct HyphenationResult {
11521 line_part: Vec<ShapedItem>,
11523 remainder_part: Vec<ShapedItem>,
11525}
11526
11527fn perform_bidi_analysis<'a>(
11528 styled_runs: &'a [TextRunInfo<'_>],
11529 full_text: &'a str,
11530 force_lang: Option<Language>,
11531) -> (Vec<VisualRun<'a>>, BidiDirection) {
11532 if full_text.is_empty() {
11533 return (Vec::new(), BidiDirection::Ltr);
11534 }
11535
11536 let bidi_info = BidiInfo::new(full_text, None);
11537 let para = &bidi_info.paragraphs[0];
11538 let base_direction = if para.level.is_rtl() {
11539 BidiDirection::Rtl
11540 } else {
11541 BidiDirection::Ltr
11542 };
11543
11544 let mut byte_to_run_index: Vec<usize> = vec![0; full_text.len()];
11546 for (run_idx, run) in styled_runs.iter().enumerate() {
11547 let start = run.logical_start;
11548 let end = start + run.text.len();
11549 for slot in &mut byte_to_run_index[start..end] {
11550 *slot = run_idx;
11551 }
11552 }
11553
11554 let mut final_visual_runs = Vec::new();
11555 let (levels, visual_run_ranges) = bidi_info.visual_runs(para, para.range.clone());
11556
11557 for range in visual_run_ranges {
11558 let bidi_level = levels[range.start];
11559 let mut sub_run_start = range.start;
11560
11561 for i in (range.start + 1)..range.end {
11563 if byte_to_run_index[i] != byte_to_run_index[sub_run_start] {
11564 let original_run_idx = byte_to_run_index[sub_run_start];
11566 let script = crate::text3::script::detect_script(&full_text[sub_run_start..i])
11567 .unwrap_or(Script::Latin);
11568 final_visual_runs.push(VisualRun {
11569 text_slice: &full_text[sub_run_start..i],
11570 style: styled_runs[original_run_idx].style.clone(),
11571 logical_start_byte: sub_run_start,
11572 bidi_level: BidiLevel::new(bidi_level.number()),
11573 language: force_lang.unwrap_or_else(|| {
11574 script_to_language(
11575 script,
11576 &full_text[sub_run_start..i],
11577 )
11578 }),
11579 script,
11580 });
11581 sub_run_start = i;
11583 }
11584 }
11585
11586 let original_run_idx = byte_to_run_index[sub_run_start];
11588 let script = crate::text3::script::detect_script(&full_text[sub_run_start..range.end])
11589 .unwrap_or(Script::Latin);
11590
11591 final_visual_runs.push(VisualRun {
11592 text_slice: &full_text[sub_run_start..range.end],
11593 style: styled_runs[original_run_idx].style.clone(),
11594 logical_start_byte: sub_run_start,
11595 bidi_level: BidiLevel::new(bidi_level.number()),
11596 script,
11597 language: force_lang.unwrap_or_else(|| {
11598 script_to_language(
11599 script,
11600 &full_text[sub_run_start..range.end],
11601 )
11602 }),
11603 });
11604 }
11605
11606 (final_visual_runs, base_direction)
11607}
11608
11609const fn get_justification_priority(class: CharacterClass) -> u8 {
11610 match class {
11611 CharacterClass::Space => 0,
11612 CharacterClass::Punctuation => 64,
11613 CharacterClass::Ideograph => 128,
11614 CharacterClass::Letter => 192,
11615 CharacterClass::Symbol => 224,
11616 CharacterClass::Combining => 255,
11617 }
11618}
11619
11620#[cfg(test)]
11621mod shape_outside_and_ruby_tests {
11622 use super::*;
11623 use azul_css::shape::{CssShape, ShapePath};
11624
11625 fn path_shape(d: &str) -> CssShape {
11626 CssShape::Path(ShapePath {
11627 data: d.into(),
11628 })
11629 }
11630
11631 #[test]
11634 fn css_path_shape_builds_path_boundary_not_rect_fallback() {
11635 let shape = path_shape("M 0 0 L 100 0 L 0 100 Z");
11637 let rbox = Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 };
11638 let boundary = ShapeBoundary::from_css_shape(&shape, rbox, &mut None);
11639 match boundary {
11640 ShapeBoundary::Path { segments } => {
11641 assert!(!segments.is_empty(), "path() must flatten to real segments");
11642 assert!(matches!(segments[0], PathSegment::MoveTo(_)));
11643 assert!(segments.iter().any(|s| matches!(s, PathSegment::Close)));
11644 }
11645 other => panic!("expected ShapeBoundary::Path, got {other:?}"),
11646 }
11647 }
11648
11649 #[test]
11650 fn empty_or_garbage_path_falls_back_to_rectangle() {
11651 let rbox = Rect { x: 0.0, y: 0.0, width: 50.0, height: 50.0 };
11652 let boundary = ShapeBoundary::from_css_shape(&path_shape(" "), rbox, &mut None);
11653 assert!(matches!(boundary, ShapeBoundary::Rectangle(_)),
11654 "unparseable path() should fall back to the reference rectangle");
11655 }
11656
11657 #[test]
11658 fn path_triangle_narrows_line_box_per_scanline() {
11659 let shape = path_shape("M 0 0 L 100 0 L 0 100 Z");
11664 let rbox = Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 };
11665 let boundary = ShapeBoundary::from_css_shape(&shape, rbox, &mut None);
11666
11667 let spans_top = get_shape_horizontal_spans(&boundary, 10.0, 1.0);
11668 let spans_bot = get_shape_horizontal_spans(&boundary, 80.0, 1.0);
11669
11670 assert_eq!(spans_top.len(), 1, "single span expected near the top");
11671 assert_eq!(spans_bot.len(), 1, "single span expected near the bottom");
11672
11673 let width_top = spans_top[0].1 - spans_top[0].0;
11674 let width_bot = spans_bot[0].1 - spans_bot[0].0;
11675
11676 assert!((width_top - 89.5).abs() < 1.5, "top width {width_top} != ~89.5");
11678 assert!((width_bot - 19.5).abs() < 1.5, "bottom width {width_bot} != ~19.5");
11679 assert!(width_top > width_bot,
11680 "path() exclusion band must narrow with y ({width_top} !> {width_bot})");
11681
11682 assert!(width_bot < 50.0, "rect fallback would give full width here");
11685 }
11686
11687 #[test]
11688 fn path_with_hole_carves_out_interior_via_even_odd() {
11689 let shape = path_shape(
11692 "M 0 0 L 100 0 L 100 100 L 0 100 Z \
11693 M 30 30 L 30 70 L 70 70 L 70 30 Z",
11694 );
11695 let rbox = Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 };
11696 let boundary = ShapeBoundary::from_css_shape(&shape, rbox, &mut None);
11697 let spans = get_shape_horizontal_spans(&boundary, 50.0, 1.0);
11698 assert_eq!(spans.len(), 2, "hole should split the band into two spans: {spans:?}");
11699 }
11700
11701 #[test]
11704 #[allow(clippy::float_cmp)] fn ruby_annotation_font_scale_is_real_not_06_fudge() {
11706 let base_font_size = 20.0_f32;
11709 let annotation_font_size = base_font_size * RUBY_ANNOTATION_FONT_SCALE;
11710 assert_eq!(annotation_font_size, 10.0);
11711 assert!((RUBY_ANNOTATION_FONT_SCALE - 0.6).abs() > f32::EPSILON,
11712 "annotation scale must not be the old 0.6 magic ratio");
11713 }
11714
11715 #[test]
11716 #[allow(clippy::float_cmp)] fn ruby_box_reserves_max_width_and_stacks_annotation_above_base() {
11718 let (w, h) = ruby_reserved_box(80.0, 30.0, 24.0, 12.0);
11720 assert_eq!(w, 80.0, "reserved width is the wider of base/annotation");
11721 assert_eq!(h, 36.0, "block-size = base line + annotation line");
11724 assert!(h > 24.0, "ruby box must reserve extra vertical space for the annotation");
11725
11726 let (w2, _) = ruby_reserved_box(20.0, 50.0, 24.0, 12.0);
11728 assert_eq!(w2, 50.0, "a long annotation widens the reserved box");
11729 }
11730}
11731
11732#[cfg(test)]
11733mod font_cache_swap_tests {
11734 use azul_css::props::basic::FontRef;
11735
11736 use super::*;
11737
11738 fn assert_index_is_live(m: &FontManager<FontRef>) {
11744 for (family, faces) in &m.memory_families {
11745 for f in faces {
11746 assert!(
11747 m.fc_cache.is_memory_font(&f.font_match.id),
11748 "`{family}` is indexed with {:?}, which the live fc_cache cannot load",
11749 f.font_match.id
11750 );
11751 }
11752 }
11753 }
11754
11755 #[test]
11756 fn swapping_the_fc_cache_does_not_strand_a_dead_memory_font_id() {
11757 let mut m: FontManager<FontRef> =
11758 FontManager::new(FcFontCache::default()).expect("FontManager::new must not fail");
11759 let norm = rust_fontconfig::utils::normalize_family_name("Azul Mock Mono");
11760
11761 let before = m
11762 .memory_families
11763 .get(&norm)
11764 .cloned()
11765 .expect("the built-in mock fonts are registered by every constructor");
11766 assert_eq!(before.len(), 1);
11767 assert_index_is_live(&m);
11768
11769 for swap in 1..=3 {
11771 m.replace_fc_cache(FcFontCache::default());
11772 assert_index_is_live(&m);
11773 let faces = m
11774 .memory_families
11775 .get(&norm)
11776 .expect("the mock fonts are re-registered into the new cache");
11777 assert_eq!(
11778 faces.len(),
11779 1,
11780 "swap {swap} appended a face instead of replacing it: the index grows by one \
11781 dead face per cache swap and `pick_memory_face` keeps returning the first \
11782 (dead) one"
11783 );
11784 }
11785
11786 let after = &m.memory_families[&norm];
11790 assert_ne!(
11791 after[0].font_match.id, before[0].font_match.id,
11792 "a fresh FcFontCache cannot already contain the mock font"
11793 );
11794 assert!(
11795 !m.fc_cache.is_memory_font(&before[0].font_match.id),
11796 "the pre-swap id must be dead — otherwise this test proves nothing"
11797 );
11798 }
11799}
11800
11801#[cfg(test)]
11809#[allow(
11810 clippy::float_cmp,
11811 clippy::too_many_lines,
11812 clippy::unreadable_literal,
11813 clippy::cast_precision_loss,
11814 clippy::similar_names
11815)]
11816mod autotest_generated {
11817 use super::*;
11818
11819 fn metrics(upem: u16, ascent: f32, descent: f32, line_gap: f32) -> LayoutFontMetrics {
11824 LayoutFontMetrics {
11825 ascent,
11826 descent,
11827 line_gap,
11828 units_per_em: upem,
11829 x_height: None,
11830 cap_height: None,
11831 }
11832 }
11833
11834 fn std_metrics() -> LayoutFontMetrics {
11836 metrics(1000, 800.0, -200.0, 0.0)
11837 }
11838
11839 fn style() -> Arc<StyleProperties> {
11840 Arc::new(StyleProperties::default())
11841 }
11842
11843 fn styled(f: impl FnOnce(&mut StyleProperties)) -> Arc<StyleProperties> {
11844 let mut s = StyleProperties::default();
11845 f(&mut s);
11846 Arc::new(s)
11847 }
11848
11849 const fn ci(run: u32, item: u32) -> ContentIndex {
11850 ContentIndex {
11851 run_index: run,
11852 item_index: item,
11853 }
11854 }
11855
11856 const fn gid(run: u32, byte: u32) -> GraphemeClusterId {
11857 GraphemeClusterId {
11858 source_run: run,
11859 start_byte_in_run: byte,
11860 }
11861 }
11862
11863 fn shaped_glyph(st: Arc<StyleProperties>, fm: LayoutFontMetrics, advance: f32) -> ShapedGlyph {
11864 ShapedGlyph {
11865 kind: GlyphKind::Character,
11866 glyph_id: 42,
11867 cluster_offset: 0,
11868 advance,
11869 kerning: 0.0,
11870 offset: Point { x: 0.0, y: 0.0 },
11871 vertical_advance: advance,
11872 vertical_offset: Point { x: 0.0, y: 0.0 },
11873 script: Script::Latin,
11874 style: st,
11875 font_hash: 0xABCD_u64,
11876 font_metrics: fm,
11877 }
11878 }
11879
11880 fn make_cluster(
11881 text: &str,
11882 advance: f32,
11883 st: Arc<StyleProperties>,
11884 glyphs: ShapedGlyphVec,
11885 id: GraphemeClusterId,
11886 ) -> ShapedItem {
11887 ShapedItem::Cluster(ShapedCluster {
11888 text: text.to_string(),
11889 source_cluster_id: id,
11890 source_content_index: ci(id.source_run, id.start_byte_in_run),
11891 source_node_id: None,
11892 glyphs,
11893 advance,
11894 direction: BidiDirection::Ltr,
11895 style: st,
11896 marker_position_outside: None,
11897 is_first_fragment: true,
11898 is_last_fragment: true,
11899 })
11900 }
11901
11902 fn cl(text: &str, advance: f32) -> ShapedItem {
11904 let st = style();
11905 let g = shaped_glyph(st.clone(), std_metrics(), advance);
11906 make_cluster(text, advance, st, smallvec![g], gid(0, 0))
11907 }
11908
11909 fn cl_at(text: &str, advance: f32, run: u32, byte: u32) -> ShapedItem {
11911 let st = style();
11912 let g = shaped_glyph(st.clone(), std_metrics(), advance);
11913 make_cluster(text, advance, st, smallvec![g], gid(run, byte))
11914 }
11915
11916 fn cl_styled(text: &str, advance: f32, st: Arc<StyleProperties>) -> ShapedItem {
11918 let g = shaped_glyph(st.clone(), std_metrics(), advance);
11919 make_cluster(text, advance, st, smallvec![g], gid(0, 0))
11920 }
11921
11922 fn cl_no_glyphs(text: &str, advance: f32) -> ShapedItem {
11924 make_cluster(text, advance, style(), ShapedGlyphVec::new(), gid(0, 0))
11925 }
11926
11927 fn obj(width: f32, height: f32, baseline_offset: f32) -> ShapedItem {
11928 ShapedItem::Object {
11929 source: ci(0, 0),
11930 bounds: Rect {
11931 x: 0.0,
11932 y: 0.0,
11933 width,
11934 height,
11935 },
11936 baseline_offset,
11937 content: InlineContent::Space(InlineSpace {
11938 width,
11939 is_breaking: false,
11940 is_stretchy: false,
11941 }),
11942 }
11943 }
11944
11945 fn brk() -> ShapedItem {
11946 ShapedItem::Break {
11947 source: ci(0, 0),
11948 break_info: InlineBreak {
11949 break_type: BreakType::Hard,
11950 clear: ClearType::None,
11951 content_index: 0,
11952 },
11953 }
11954 }
11955
11956 fn tab(width: f32, height: f32) -> ShapedItem {
11957 ShapedItem::Tab {
11958 source: ci(0, 0),
11959 bounds: Rect {
11960 x: 0.0,
11961 y: 0.0,
11962 width,
11963 height,
11964 },
11965 }
11966 }
11967
11968 fn pos(item: ShapedItem, x: f32, y: f32, line_index: usize) -> PositionedItem {
11969 PositionedItem {
11970 item,
11971 position: Point { x, y },
11972 line_index,
11973 }
11974 }
11975
11976 fn text_content(t: &str, st: Arc<StyleProperties>) -> InlineContent {
11977 InlineContent::Text(StyledRun {
11978 text: t.to_string(),
11979 style: st,
11980 logical_start_byte: 0,
11981 source_node_id: None,
11982 })
11983 }
11984
11985 fn sel(family: &str) -> FontSelector {
11986 FontSelector {
11987 family: family.to_string(),
11988 ..FontSelector::default()
11989 }
11990 }
11991
11992 #[derive(Debug, Clone)]
11995 struct TestFont {
11996 hash: u64,
11997 }
11998
11999 impl ShallowClone for TestFont {
12000 fn shallow_clone(&self) -> Self {
12001 self.clone()
12002 }
12003 }
12004
12005 impl ParsedFontTrait for TestFont {
12006 fn shape_text(
12007 &self,
12008 _text: &str,
12009 _script: Script,
12010 _language: Language,
12011 _direction: BidiDirection,
12012 _style: &StyleProperties,
12013 ) -> Result<Vec<Glyph>, LayoutError> {
12014 Ok(Vec::new())
12015 }
12016 fn get_hash(&self) -> u64 {
12017 self.hash
12018 }
12019 fn get_glyph_size(&self, _glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
12020 Some(LogicalSize {
12021 width: font_size,
12022 height: font_size,
12023 })
12024 }
12025 fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
12026 Some((1, font_size * 0.3))
12027 }
12028 fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
12029 Some((2, font_size * 0.2))
12030 }
12031 fn has_glyph(&self, _codepoint: u32) -> bool {
12032 true
12033 }
12034 fn get_vertical_metrics(&self, _glyph_id: u16) -> Option<VerticalMetrics> {
12035 None
12036 }
12037 fn get_font_metrics(&self) -> LayoutFontMetrics {
12038 std_metrics()
12039 }
12040 fn num_glyphs(&self) -> u16 {
12041 10
12042 }
12043 fn get_space_width(&self) -> Option<usize> {
12044 Some(500)
12045 }
12046 }
12047
12048 fn hash_of<T: Hash>(v: &T) -> u64 {
12049 let mut h = DefaultHasher::new();
12050 v.hash(&mut h);
12051 h.finish()
12052 }
12053
12054 #[track_caller]
12057 fn approx(actual: f32, expected: f32) {
12058 assert!(
12059 (actual - expected).abs() < 1e-4,
12060 "expected ~{expected}, got {actual}"
12061 );
12062 }
12063
12064 #[test]
12069 fn ruby_reserved_box_zero_and_negative_are_deterministic() {
12070 assert_eq!(ruby_reserved_box(0.0, 0.0, 0.0, 0.0), (0.0, 0.0));
12071 let (w, h) = ruby_reserved_box(-10.0, -4.0, -3.0, -2.0);
12073 assert_eq!(w, -4.0);
12074 assert_eq!(h, -5.0);
12075 }
12076
12077 #[test]
12078 fn ruby_reserved_box_nan_width_is_ignored_by_max_but_poisons_height() {
12079 let (w, h) = ruby_reserved_box(f32::NAN, 30.0, 24.0, f32::NAN);
12082 assert_eq!(w, 30.0, "f32::max discards the NaN operand");
12083 assert!(h.is_nan(), "but the additive block-size does propagate NaN");
12084 }
12085
12086 #[test]
12087 fn ruby_reserved_box_infinities_do_not_panic() {
12088 let (w, h) = ruby_reserved_box(f32::INFINITY, 10.0, f32::INFINITY, f32::NEG_INFINITY);
12089 assert!(w.is_infinite() && w.is_sign_positive());
12090 assert!(h.is_nan(), "inf + -inf is NaN, not a panic");
12091 }
12092
12093 #[test]
12094 fn ruby_reserved_box_saturates_to_infinity_at_f32_max() {
12095 let (w, h) = ruby_reserved_box(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
12096 assert_eq!(w, f32::MAX);
12097 assert!(h.is_infinite(), "f32 addition saturates, it does not panic");
12098 }
12099
12100 #[test]
12105 fn line_height_px_ignores_every_font_metric() {
12106 let lh = LineHeight::Px(7.5);
12107 assert_eq!(lh.resolve(16.0, 800.0, -200.0, 0.0, 1000), 7.5);
12108 assert_eq!(lh.resolve(f32::NAN, f32::NAN, f32::NAN, f32::NAN, 0), 7.5);
12110 }
12111
12112 #[test]
12113 fn line_height_normal_zero_upem_falls_back_to_1_2_em() {
12114 let lh = LineHeight::Normal;
12115 assert_eq!(lh.resolve(16.0, 800.0, -200.0, 0.0, 0), 19.2);
12116 assert_eq!(lh.resolve(0.0, 800.0, -200.0, 0.0, 0), 0.0);
12117 }
12118
12119 #[test]
12120 fn line_height_normal_scales_ascent_minus_descent_plus_gap() {
12121 approx(LineHeight::Normal.resolve(16.0, 800.0, -200.0, 0.0, 1000), 16.0);
12123 approx(
12125 LineHeight::Normal.resolve(16.0, 800.0, -200.0, 250.0, 1000),
12126 20.0,
12127 );
12128 approx(LineHeight::Normal.resolve(16.0, 800.0, 200.0, 0.0, 1000), 9.6);
12131 }
12132
12133 #[test]
12134 fn line_height_normal_at_u16_max_upem_does_not_panic() {
12135 let v = LineHeight::Normal.resolve(16.0, 800.0, -200.0, 0.0, u16::MAX);
12136 assert!(v.is_finite() && v > 0.0, "got {v}");
12137 assert!(v < 1.0, "a 65535-upem font must produce a tiny scale, got {v}");
12138 }
12139
12140 #[test]
12141 fn line_height_normal_nan_and_inf_inputs_are_defined_not_panics() {
12142 assert!(LineHeight::Normal
12143 .resolve(f32::NAN, 800.0, -200.0, 0.0, 1000)
12144 .is_nan());
12145 assert!(LineHeight::Normal
12146 .resolve(f32::INFINITY, 800.0, -200.0, 0.0, 1000)
12147 .is_infinite());
12148 assert!(LineHeight::Normal
12150 .resolve(16.0, f32::INFINITY, f32::INFINITY, 0.0, 1000)
12151 .is_nan());
12152 }
12153
12154 #[test]
12155 fn line_height_resolve_with_metrics_matches_resolve() {
12156 let fm = metrics(2048, 1600.0, -400.0, 100.0);
12157 let lh = LineHeight::Normal;
12158 assert_eq!(
12159 lh.resolve_with_metrics(24.0, &fm),
12160 lh.resolve(24.0, fm.ascent, fm.descent, fm.line_gap, fm.units_per_em)
12161 );
12162 assert_eq!(LineHeight::Px(3.0).resolve_with_metrics(24.0, &fm), 3.0);
12164 }
12165
12166 #[test]
12167 fn line_height_px_nan_is_self_equal_under_the_manual_partialeq() {
12168 assert_eq!(LineHeight::Px(f32::NAN), LineHeight::Px(f32::NAN));
12171 assert_eq!(
12172 hash_of(&LineHeight::Px(f32::NAN)),
12173 hash_of(&LineHeight::Px(f32::NAN))
12174 );
12175 assert_ne!(LineHeight::Normal, LineHeight::Px(0.0));
12176 }
12177
12178 #[test]
12183 fn available_space_definite_and_indefinite_are_exact_complements() {
12184 for v in [
12185 AvailableSpace::Definite(0.0),
12186 AvailableSpace::Definite(-1.0),
12187 AvailableSpace::Definite(f32::NAN),
12188 AvailableSpace::MinContent,
12189 AvailableSpace::MaxContent,
12190 ] {
12191 assert_ne!(v.is_definite(), v.is_indefinite(), "{v:?}");
12192 }
12193 assert!(AvailableSpace::Definite(f32::NAN).is_definite());
12194 assert!(AvailableSpace::default().is_indefinite());
12195 assert_eq!(AvailableSpace::default(), AvailableSpace::MaxContent);
12196 }
12197
12198 #[test]
12199 fn available_space_unwrap_or_returns_definite_even_when_nan_or_inf() {
12200 assert_eq!(AvailableSpace::Definite(0.0).unwrap_or(99.0), 0.0);
12201 assert_eq!(AvailableSpace::Definite(-5.0).unwrap_or(99.0), -5.0);
12202 assert!(AvailableSpace::Definite(f32::NAN).unwrap_or(99.0).is_nan());
12203 assert!(AvailableSpace::Definite(f32::INFINITY)
12204 .unwrap_or(99.0)
12205 .is_infinite());
12206 assert_eq!(AvailableSpace::MinContent.unwrap_or(99.0), 99.0);
12208 assert_eq!(AvailableSpace::MaxContent.unwrap_or(-0.0), -0.0);
12209 assert!(AvailableSpace::MaxContent.unwrap_or(f32::NAN).is_nan());
12210 }
12211
12212 #[test]
12213 fn available_space_to_f32_for_layout_uses_half_max_for_both_intrinsic_modes() {
12214 assert_eq!(AvailableSpace::MinContent.to_f32_for_layout(), f32::MAX / 2.0);
12215 assert_eq!(AvailableSpace::MaxContent.to_f32_for_layout(), f32::MAX / 2.0);
12216 assert_eq!(AvailableSpace::Definite(12.5).to_f32_for_layout(), 12.5);
12217 assert!(AvailableSpace::Definite(f32::NAN)
12218 .to_f32_for_layout()
12219 .is_nan());
12220 }
12221
12222 #[test]
12223 fn available_space_from_f32_sentinels() {
12224 assert_eq!(AvailableSpace::from_f32(f32::INFINITY), AvailableSpace::MaxContent);
12225 assert_eq!(AvailableSpace::from_f32(f32::MAX), AvailableSpace::MaxContent);
12226 assert_eq!(
12228 AvailableSpace::from_f32(f32::MAX / 2.0),
12229 AvailableSpace::MaxContent
12230 );
12231 assert_eq!(AvailableSpace::from_f32(0.0), AvailableSpace::MinContent);
12232 assert_eq!(AvailableSpace::from_f32(-0.0), AvailableSpace::MinContent);
12233 assert_eq!(AvailableSpace::from_f32(-1.0), AvailableSpace::MinContent);
12234 assert_eq!(AvailableSpace::from_f32(100.0), AvailableSpace::Definite(100.0));
12235 }
12236
12237 #[test]
12238 fn available_space_from_f32_negative_infinity_becomes_max_content() {
12239 assert_eq!(
12243 AvailableSpace::from_f32(f32::NEG_INFINITY),
12244 AvailableSpace::MaxContent
12245 );
12246 }
12247
12248 #[test]
12249 fn available_space_from_f32_nan_falls_through_to_definite_nan() {
12250 let got = AvailableSpace::from_f32(f32::NAN);
12253 match got {
12254 AvailableSpace::Definite(v) => assert!(v.is_nan(), "expected Definite(NaN)"),
12255 other => panic!("NaN should fall through to Definite, got {other:?}"),
12256 }
12257 assert_ne!(got, AvailableSpace::from_f32(f32::NAN));
12259 }
12260
12261 #[test]
12262 fn available_space_hash_eq_contract_holds_for_signed_zero() {
12263 assert_eq!(
12265 AvailableSpace::Definite(0.0),
12266 AvailableSpace::Definite(-0.0)
12267 );
12268 assert_eq!(
12269 hash_of(&AvailableSpace::Definite(0.0)),
12270 hash_of(&AvailableSpace::Definite(-0.0))
12271 );
12272 assert_ne!(
12274 hash_of(&AvailableSpace::Definite(100.1)),
12275 hash_of(&AvailableSpace::Definite(100.4))
12276 );
12277 assert_ne!(
12278 hash_of(&AvailableSpace::MinContent),
12279 hash_of(&AvailableSpace::MaxContent)
12280 );
12281 }
12282
12283 #[test]
12288 fn font_chain_key_from_empty_selectors_defaults_to_serif() {
12289 let k = FontChainKey::from_selectors(&[]);
12290 assert_eq!(k.font_families, vec!["serif".to_string()]);
12291 assert_eq!(k.weight, FcWeight::Normal);
12292 assert!(!k.italic && !k.oblique);
12293 }
12294
12295 #[test]
12296 fn font_chain_key_dedups_first_wins_and_skips_empty_families() {
12297 let stack = [sel("Arial"), sel("Times"), sel("Arial"), sel("")];
12298 let k = FontChainKey::from_selectors(&stack);
12299 assert_eq!(
12300 k.font_families,
12301 vec!["Arial".to_string(), "Times".to_string()],
12302 "duplicate families must collapse first-wins, empty names dropped"
12303 );
12304 }
12305
12306 #[test]
12307 fn font_chain_key_all_empty_families_still_yields_serif() {
12308 let stack = [sel(""), sel(""), sel("")];
12309 let k = FontChainKey::from_selectors(&stack);
12310 assert_eq!(k.font_families, vec!["serif".to_string()]);
12311 }
12312
12313 #[test]
12314 fn font_chain_key_weight_and_style_come_from_the_first_selector_even_if_it_is_dropped() {
12315 let mut first = sel("");
12318 first.style = FontStyle::Italic;
12319 first.weight = FcWeight::Bold;
12320 let stack = [first, sel("Arial")];
12321 let k = FontChainKey::from_selectors(&stack);
12322 assert_eq!(k.font_families, vec!["Arial".to_string()]);
12323 assert_eq!(k.weight, FcWeight::Bold);
12324 assert!(k.italic, "italic taken from the dropped first selector");
12325 assert!(!k.oblique);
12326 }
12327
12328 #[test]
12329 fn font_chain_key_oblique_is_exclusive_of_italic() {
12330 let mut s = sel("Arial");
12331 s.style = FontStyle::Oblique;
12332 let k = FontChainKey::from_selectors(&[s]);
12333 assert!(k.oblique && !k.italic);
12334 }
12335
12336 #[test]
12337 fn font_chain_key_huge_duplicate_stack_does_not_hang() {
12338 let stack: Vec<FontSelector> = (0..5000).map(|_| sel("Arial")).collect();
12339 let k = FontChainKey::from_selectors(&stack);
12340 assert_eq!(k.font_families.len(), 1, "5000 dupes collapse to one entry");
12341 }
12342
12343 #[test]
12344 fn font_chain_key_is_a_stable_hash_map_key() {
12345 let a = FontChainKey::from_selectors(&[sel("Arial"), sel("Times")]);
12346 let b = FontChainKey::from_selectors(&[sel("Arial"), sel("Arial"), sel("Times")]);
12347 assert_eq!(a, b, "dedup makes the two stacks resolve to the same key");
12348 assert_eq!(hash_of(&a), hash_of(&b));
12349 }
12350
12351 #[test]
12352 fn font_chain_key_or_ref_from_stack_is_a_chain() {
12353 let fs = FontStack::Stack(vec![sel("Arial")]);
12354 let k = FontChainKeyOrRef::from_font_stack(&fs);
12355 assert!(!k.is_ref());
12356 assert_eq!(k.as_ref_ptr(), None);
12357 assert_eq!(
12358 k.as_chain().map(|c| c.font_families.clone()),
12359 Some(vec!["Arial".to_string()])
12360 );
12361 }
12362
12363 #[test]
12364 fn font_chain_key_or_ref_ref_variant_accessors_at_boundaries() {
12365 for ptr in [0_usize, 1, usize::MAX] {
12366 let k = FontChainKeyOrRef::Ref(ptr);
12367 assert!(k.is_ref());
12368 assert_eq!(k.as_ref_ptr(), Some(ptr));
12369 assert!(k.as_chain().is_none());
12370 }
12371 assert_ne!(
12373 FontChainKeyOrRef::Ref(0),
12374 FontChainKeyOrRef::Chain(FontChainKey::from_selectors(&[]))
12375 );
12376 }
12377
12378 #[test]
12379 fn font_stack_default_is_a_single_serif_selector() {
12380 let fs = FontStack::default();
12381 assert!(!fs.is_ref());
12382 assert!(fs.as_ref().is_none());
12383 assert_eq!(fs.as_stack().map(<[FontSelector]>::len), Some(1));
12384 assert_eq!(fs.first_selector().map(|s| s.family.as_str()), Some("serif"));
12385 assert_eq!(fs.first_family(), "serif");
12386 }
12387
12388 #[test]
12389 fn font_stack_empty_stack_reports_serif_placeholder_but_no_first_selector() {
12390 let fs = FontStack::Stack(Vec::new());
12391 assert_eq!(fs.as_stack().map(<[FontSelector]>::len), Some(0));
12392 assert!(fs.first_selector().is_none());
12393 assert_eq!(
12394 fs.first_family(),
12395 "serif",
12396 "an EMPTY stack must not panic; it reports the serif fallback"
12397 );
12398 }
12399
12400 #[test]
12401 fn font_hash_invalid_is_zero_and_is_the_default() {
12402 assert_eq!(FontHash::invalid().font_hash, 0);
12403 assert_eq!(FontHash::default(), FontHash::invalid());
12404 assert_eq!(FontHash::from_hash(0), FontHash::invalid());
12405 assert_eq!(FontHash::from_hash(u64::MAX).font_hash, u64::MAX);
12406 assert_ne!(FontHash::from_hash(u64::MAX), FontHash::invalid());
12407 }
12408
12409 #[test]
12414 fn layout_font_metrics_baseline_scaled_typical_and_zero_font_size() {
12415 let fm = std_metrics();
12416 approx(fm.baseline_scaled(16.0), 12.8); assert_eq!(fm.baseline_scaled(0.0), 0.0);
12418 approx(fm.baseline_scaled(-16.0), -12.8);
12419 }
12420
12421 #[test]
12422 fn layout_font_metrics_zero_upem_divides_by_zero_instead_of_guarding() {
12423 let fm = metrics(0, 800.0, -200.0, 0.0);
12427 assert!(fm.baseline_scaled(16.0).is_infinite());
12428 assert!(fm.cap_height_scaled(16.0).is_infinite());
12429
12430 let zero = metrics(0, 0.0, 0.0, 0.0);
12432 assert!(zero.baseline_scaled(16.0).is_nan());
12433 }
12434
12435 #[test]
12436 fn layout_font_metrics_x_height_falls_back_to_half_em() {
12437 let fm = std_metrics(); assert_eq!(fm.x_height_scaled(16.0), 8.0, "fallback is 0.5em");
12439 assert_eq!(fm.x_height_scaled(0.0), 0.0);
12440
12441 let mut with_xh = std_metrics();
12442 with_xh.x_height = Some(500.0);
12443 approx(with_xh.x_height_scaled(16.0), 8.0);
12444 with_xh.x_height = Some(0.0);
12445 assert_eq!(
12446 with_xh.x_height_scaled(16.0),
12447 0.0,
12448 "an explicit sxHeight of 0 must NOT re-trigger the 0.5em fallback"
12449 );
12450 }
12451
12452 #[test]
12453 fn layout_font_metrics_cap_height_falls_back_to_ascent() {
12454 let fm = std_metrics(); assert_eq!(fm.cap_height_scaled(16.0), fm.baseline_scaled(16.0));
12456
12457 let mut with_cap = std_metrics();
12458 with_cap.cap_height = Some(700.0);
12459 approx(with_cap.cap_height_scaled(16.0), 11.2);
12460 }
12461
12462 #[test]
12463 fn layout_font_metrics_nan_font_size_propagates_without_panicking() {
12464 let fm = std_metrics();
12465 assert!(fm.baseline_scaled(f32::NAN).is_nan());
12466 assert!(fm.x_height_scaled(f32::NAN).is_nan());
12467 assert!(fm.cap_height_scaled(f32::NAN).is_nan());
12468 assert!(fm.baseline_scaled(f32::INFINITY).is_infinite());
12469 }
12470
12471 #[test]
12472 fn layout_font_metrics_synthesized_baselines_span_exactly_one_em() {
12473 let fm = std_metrics();
12474 assert_eq!(fm.central_baseline(), 300.0); assert_eq!(fm.em_over(), 800.0); assert_eq!(fm.em_under(), -200.0); assert_eq!(
12478 fm.em_over() - fm.em_under(),
12479 f32::from(fm.units_per_em),
12480 "em-over minus em-under is by definition 1em"
12481 );
12482 }
12483
12484 #[test]
12485 fn layout_font_metrics_baselines_at_u16_max_upem_and_zero_upem() {
12486 let big = metrics(u16::MAX, 0.0, 0.0, 0.0);
12487 assert_eq!(big.central_baseline(), 0.0);
12488 assert_eq!(big.em_over(), f32::from(u16::MAX) / 2.0);
12489 assert_eq!(big.em_under(), -f32::from(u16::MAX) / 2.0);
12490
12491 let zero = metrics(0, 100.0, -50.0, 0.0);
12492 assert_eq!(zero.em_over(), zero.central_baseline());
12493 assert_eq!(zero.em_under(), zero.central_baseline());
12494 }
12495
12496 #[test]
12497 fn layout_font_metrics_central_baseline_with_infinite_extents_is_nan() {
12498 let fm = metrics(1000, f32::INFINITY, f32::NEG_INFINITY, 0.0);
12499 assert!(fm.central_baseline().is_nan(), "midpoint(inf, -inf) is NaN");
12500 assert!(fm.em_over().is_nan());
12501 }
12502
12503 #[test]
12508 fn round_eq_rounds_half_away_from_zero() {
12509 assert!(round_eq(0.4, -0.4), "both round to 0");
12510 assert!(round_eq(1.5, 2.4), "1.5 rounds away from zero to 2");
12511 assert!(!round_eq(1.4, 1.5));
12512 assert!(round_eq(-1.5, -2.0));
12513 }
12514
12515 #[test]
12516 fn round_eq_treats_nan_as_equal_to_everything_rounding_to_zero() {
12517 assert!(round_eq(f32::NAN, f32::NAN));
12522 assert!(round_eq(f32::NAN, 0.0));
12523 assert!(round_eq(f32::NAN, 0.49));
12524 assert!(!round_eq(f32::NAN, 1.0));
12525
12526 assert_eq!(
12527 Rect {
12528 x: f32::NAN,
12529 y: 0.0,
12530 width: 0.0,
12531 height: 0.0
12532 },
12533 Rect::default(),
12534 "a NaN-x Rect compares equal to the zero Rect"
12535 );
12536 }
12537
12538 #[test]
12539 fn round_eq_saturates_infinity_and_f32_max_to_the_same_isize() {
12540 assert!(round_eq(f32::INFINITY, f32::MAX));
12542 assert!(round_eq(f32::NEG_INFINITY, f32::MIN));
12543 assert!(!round_eq(f32::INFINITY, f32::NEG_INFINITY));
12544
12545 assert_eq!(
12546 Size::new(f32::INFINITY, 0.0),
12547 Size::new(f32::MAX, 0.0),
12548 "saturating cast collapses inf and f32::MAX into one bucket"
12549 );
12550 }
12551
12552 #[test]
12557 fn size_zero_is_the_neutral_element_and_new_preserves_bits() {
12558 assert_eq!(Size::zero(), Size::new(0.0, 0.0));
12559 assert_eq!(Size::zero().width, 0.0);
12560 assert_eq!(Size::zero(), Size::default());
12561
12562 let weird = Size::new(f32::NAN, f32::INFINITY);
12563 assert!(weird.width.is_nan(), "the constructor must not sanitize");
12564 assert!(weird.height.is_infinite());
12565 }
12566
12567 #[test]
12568 fn bounding_box_of_empty_and_single_point_is_zero() {
12569 assert_eq!(calculate_bounding_box_size(&[]), Size::zero());
12570 assert_eq!(
12571 calculate_bounding_box_size(&[Point { x: 5.0, y: -5.0 }]),
12572 Size::zero()
12573 );
12574 }
12575
12576 #[test]
12577 fn bounding_box_spans_negative_coordinates() {
12578 let pts = [
12579 Point { x: -10.0, y: -20.0 },
12580 Point { x: 30.0, y: 5.0 },
12581 Point { x: 0.0, y: 0.0 },
12582 ];
12583 assert_eq!(calculate_bounding_box_size(&pts), Size::new(40.0, 25.0));
12584 }
12585
12586 #[test]
12587 fn bounding_box_of_all_nan_points_collapses_to_zero() {
12588 let pts = [Point {
12590 x: f32::NAN,
12591 y: f32::NAN,
12592 }];
12593 assert_eq!(calculate_bounding_box_size(&pts), Size::zero());
12594 }
12595
12596 #[test]
12597 fn bounding_box_of_extreme_points_overflows_to_infinity_without_panicking() {
12598 let pts = [
12599 Point {
12600 x: f32::MIN,
12601 y: f32::MIN,
12602 },
12603 Point {
12604 x: f32::MAX,
12605 y: f32::MAX,
12606 },
12607 ];
12608 let s = calculate_bounding_box_size(&pts);
12609 assert!(s.width.is_infinite() && s.height.is_infinite());
12610 }
12611
12612 #[test]
12613 fn shape_definition_get_size_for_each_variant() {
12614 assert_eq!(
12615 ShapeDefinition::Rectangle {
12616 size: Size::new(3.0, 4.0),
12617 corner_radius: None
12618 }
12619 .get_size(),
12620 Size::new(3.0, 4.0)
12621 );
12622 assert_eq!(
12623 ShapeDefinition::Circle { radius: 10.0 }.get_size(),
12624 Size::new(20.0, 20.0)
12625 );
12626 assert_eq!(
12627 ShapeDefinition::Ellipse {
12628 radii: Size::new(5.0, 2.0)
12629 }
12630 .get_size(),
12631 Size::new(10.0, 4.0)
12632 );
12633 assert_eq!(
12634 ShapeDefinition::Polygon { points: Vec::new() }.get_size(),
12635 Size::zero()
12636 );
12637 assert_eq!(
12638 ShapeDefinition::Path {
12639 segments: Vec::new()
12640 }
12641 .get_size(),
12642 Size::zero()
12643 );
12644 }
12645
12646 #[test]
12647 fn shape_definition_negative_circle_radius_yields_a_negative_size() {
12648 let s = ShapeDefinition::Circle { radius: -10.0 }.get_size();
12651 assert_eq!(s.width, -20.0);
12652 assert_eq!(s.height, -20.0);
12653 }
12654
12655 #[test]
12656 fn shape_definition_path_of_only_close_segments_is_zero_sized() {
12657 let s = ShapeDefinition::Path {
12658 segments: vec![PathSegment::Close, PathSegment::Close],
12659 }
12660 .get_size();
12661 assert_eq!(s, Size::zero(), "Close contributes no points");
12662 }
12663
12664 #[test]
12665 fn shape_definition_path_bounding_box_includes_control_points() {
12666 let s = ShapeDefinition::Path {
12667 segments: vec![
12668 PathSegment::MoveTo(Point { x: 0.0, y: 0.0 }),
12669 PathSegment::QuadTo {
12670 control: Point { x: 50.0, y: 100.0 },
12671 end: Point { x: 100.0, y: 0.0 },
12672 },
12673 ],
12674 }
12675 .get_size();
12676 assert_eq!(
12677 s,
12678 Size::new(100.0, 100.0),
12679 "the control point (not the true curve extremum) sets the height"
12680 );
12681 }
12682
12683 #[test]
12688 fn shape_boundary_inflate_by_zero_is_identity() {
12689 let r = ShapeBoundary::Rectangle(Rect {
12690 x: 1.0,
12691 y: 2.0,
12692 width: 3.0,
12693 height: 4.0,
12694 });
12695 assert_eq!(r.inflate(0.0), r);
12696 let c = ShapeBoundary::Circle {
12697 center: Point { x: 0.0, y: 0.0 },
12698 radius: 5.0,
12699 };
12700 assert_eq!(c.inflate(0.0), c);
12701 }
12702
12703 #[test]
12704 fn shape_boundary_inflate_rectangle_clamps_negative_dimensions_to_zero() {
12705 let r = ShapeBoundary::Rectangle(Rect {
12706 x: 0.0,
12707 y: 0.0,
12708 width: 10.0,
12709 height: 10.0,
12710 });
12711 match r.inflate(-100.0) {
12712 ShapeBoundary::Rectangle(out) => {
12713 assert_eq!(out.width, 0.0, "over-deflation must clamp, not go negative");
12714 assert_eq!(out.height, 0.0);
12715 assert_eq!(out.x, 100.0, "the origin is NOT clamped");
12716 }
12717 other => panic!("expected Rectangle, got {other:?}"),
12718 }
12719 }
12720
12721 #[test]
12722 fn shape_boundary_inflate_nan_margin_zeroes_the_rectangle_extent() {
12723 let r = ShapeBoundary::Rectangle(Rect {
12727 x: 0.0,
12728 y: 0.0,
12729 width: 10.0,
12730 height: 10.0,
12731 });
12732 match r.inflate(f32::NAN) {
12733 ShapeBoundary::Rectangle(out) => {
12734 assert_eq!(out.width, 0.0);
12735 assert_eq!(out.height, 0.0);
12736 assert!(out.x.is_nan());
12737 }
12738 other => panic!("expected Rectangle, got {other:?}"),
12739 }
12740 }
12741
12742 #[test]
12743 fn shape_boundary_inflate_circle_radius_is_unclamped() {
12744 let c = ShapeBoundary::Circle {
12745 center: Point { x: 1.0, y: 2.0 },
12746 radius: 5.0,
12747 };
12748 match c.inflate(-50.0) {
12749 ShapeBoundary::Circle { center, radius } => {
12750 assert_eq!(center, Point { x: 1.0, y: 2.0 });
12751 assert_eq!(radius, -45.0, "circle radius is NOT clamped at 0 (unlike Rect)");
12752 }
12753 other => panic!("expected Circle, got {other:?}"),
12754 }
12755 match c.inflate(f32::INFINITY) {
12756 ShapeBoundary::Circle { radius, .. } => assert!(radius.is_infinite()),
12757 other => panic!("expected Circle, got {other:?}"),
12758 }
12759 }
12760
12761 #[test]
12762 fn shape_boundary_inflate_is_a_documented_no_op_for_polygon_and_path() {
12763 let p = ShapeBoundary::Polygon {
12764 points: vec![Point { x: 0.0, y: 0.0 }, Point { x: 1.0, y: 1.0 }],
12765 };
12766 assert_eq!(p.inflate(10.0), p, "polygon inflation is not implemented");
12767 let path = ShapeBoundary::Path {
12768 segments: vec![PathSegment::MoveTo(Point { x: 0.0, y: 0.0 })],
12769 };
12770 assert_eq!(path.inflate(10.0), path, "path inflation is not implemented");
12771 }
12772
12773 #[test]
12778 fn resolve_effective_alignment_passes_through_for_non_last_lines() {
12779 for ta in [
12780 TextAlign::Left,
12781 TextAlign::Right,
12782 TextAlign::Center,
12783 TextAlign::Justify,
12784 TextAlign::Start,
12785 TextAlign::End,
12786 TextAlign::JustifyAll,
12787 ] {
12788 assert_eq!(
12789 resolve_effective_alignment(ta, TextAlign::Right, false),
12790 ta,
12791 "text-align-last must not touch a non-last line"
12792 );
12793 }
12794 }
12795
12796 #[test]
12797 fn resolve_effective_alignment_last_line_justify_degrades_to_start() {
12798 assert_eq!(
12799 resolve_effective_alignment(TextAlign::Justify, TextAlign::default(), true),
12800 TextAlign::Start
12801 );
12802 assert_eq!(
12803 resolve_effective_alignment(TextAlign::Center, TextAlign::default(), true),
12804 TextAlign::Center,
12805 "non-justify alignments survive onto the last line"
12806 );
12807 }
12808
12809 #[test]
12810 fn resolve_effective_alignment_explicit_text_align_last_left_is_indistinguishable_from_auto() {
12811 assert_eq!(
12815 resolve_effective_alignment(TextAlign::Center, TextAlign::Left, true),
12816 TextAlign::Center
12817 );
12818 assert_eq!(
12820 resolve_effective_alignment(TextAlign::Center, TextAlign::Right, true),
12821 TextAlign::Right
12822 );
12823 assert_eq!(
12824 resolve_effective_alignment(TextAlign::Justify, TextAlign::Justify, true),
12825 TextAlign::Justify
12826 );
12827 }
12828
12829 #[test]
12834 fn spacing_resolve_px_default_is_zero_and_font_size_independent() {
12835 assert_eq!(Spacing::default(), Spacing::Px(0));
12836 assert_eq!(Spacing::default().resolve_px(16.0), 0.0);
12837 assert_eq!(Spacing::Px(0).resolve_px(f32::NAN), 0.0);
12838 }
12839
12840 #[test]
12841 fn spacing_resolve_px_at_i32_extremes_stays_finite() {
12842 let hi = Spacing::Px(i32::MAX).resolve_px(16.0);
12843 let lo = Spacing::Px(i32::MIN).resolve_px(16.0);
12844 assert!(hi.is_finite() && hi > 2.0e9, "got {hi}");
12845 assert!(lo.is_finite() && lo < -2.0e9, "got {lo}");
12846 assert_eq!(lo, -2147483648.0);
12847 }
12848
12849 #[test]
12850 fn spacing_resolve_px_em_scales_with_font_size() {
12851 assert_eq!(Spacing::Em(2.0).resolve_px(16.0), 32.0);
12852 assert_eq!(Spacing::Em(2.0).resolve_px(0.0), 0.0);
12853 assert_eq!(Spacing::Em(-0.5).resolve_px(16.0), -8.0);
12854 assert_eq!(Spacing::PxF(0.4).resolve_px(999.0), 0.4, "PxF ignores font size");
12855 }
12856
12857 #[test]
12858 fn spacing_resolve_px_nan_and_overflow_are_defined() {
12859 assert!(Spacing::Em(f32::NAN).resolve_px(16.0).is_nan());
12860 assert!(Spacing::Em(1.0).resolve_px(f32::NAN).is_nan());
12861 assert!(Spacing::PxF(f32::NAN).resolve_px(16.0).is_nan());
12862 assert!(Spacing::Em(f32::MAX).resolve_px(2.0).is_infinite());
12863 assert!(Spacing::Em(0.0).resolve_px(f32::INFINITY).is_nan());
12865 }
12866
12867 #[test]
12868 fn spacing_px_and_pxf_of_the_same_value_are_distinct_cache_keys() {
12869 assert_ne!(Spacing::Px(1), Spacing::PxF(1.0));
12870 assert_ne!(hash_of(&Spacing::Px(1)), hash_of(&Spacing::PxF(1.0)));
12871 assert_eq!(Spacing::Px(1).resolve_px(16.0), Spacing::PxF(1.0).resolve_px(16.0));
12872 }
12873
12874 #[test]
12879 fn bidi_direction_is_rtl() {
12880 assert!(!BidiDirection::Ltr.is_rtl());
12881 assert!(BidiDirection::Rtl.is_rtl());
12882 }
12883
12884 #[test]
12885 fn bidi_level_parity_defines_rtl_across_the_whole_u8_range() {
12886 for lvl in [0_u8, 1, 2, 3, 126, 127, 254, u8::MAX] {
12887 let b = BidiLevel::new(lvl);
12888 assert_eq!(b.level(), lvl, "level() must round-trip new()");
12889 assert_eq!(b.is_rtl(), lvl % 2 == 1, "odd embedding levels are RTL");
12890 }
12891 }
12892
12893 #[test]
12894 fn writing_mode_is_advance_horizontal_for_every_variant() {
12895 assert!(WritingMode::HorizontalTb.is_advance_horizontal());
12896 assert!(WritingMode::SidewaysRl.is_advance_horizontal());
12897 assert!(WritingMode::SidewaysLr.is_advance_horizontal());
12898 assert!(!WritingMode::VerticalRl.is_advance_horizontal());
12899 assert!(!WritingMode::VerticalLr.is_advance_horizontal());
12900 assert_eq!(WritingMode::default(), WritingMode::HorizontalTb);
12901 }
12902
12903 #[test]
12904 fn writing_mode_get_direction_only_horizontal_defers_to_content() {
12905 assert_eq!(WritingMode::HorizontalTb.get_direction(), None);
12906 assert_eq!(WritingMode::VerticalRl.get_direction(), Some(BidiDirection::Rtl));
12907 assert_eq!(WritingMode::VerticalLr.get_direction(), Some(BidiDirection::Ltr));
12908 assert_eq!(WritingMode::SidewaysRl.get_direction(), Some(BidiDirection::Rtl));
12909 assert_eq!(WritingMode::SidewaysLr.get_direction(), Some(BidiDirection::Ltr));
12910 }
12911
12912 #[test]
12917 fn unified_constraints_default_is_horizontal_max_content() {
12918 let c = UnifiedConstraints::default();
12919 assert!(!c.is_vertical());
12920 assert_eq!(c.available_width, AvailableSpace::MaxContent);
12921 assert_eq!(c.columns, 1);
12922 assert_eq!(c, UnifiedConstraints::default());
12923 assert_eq!(
12924 hash_of(&c),
12925 hash_of(&UnifiedConstraints::default()),
12926 "Hash/Eq must agree for the default constraints"
12927 );
12928 }
12929
12930 #[test]
12931 fn unified_constraints_is_vertical_only_for_the_two_vertical_modes() {
12932 let mut c = UnifiedConstraints::default();
12933 for (wm, want) in [
12934 (WritingMode::HorizontalTb, false),
12935 (WritingMode::VerticalRl, true),
12936 (WritingMode::VerticalLr, true),
12937 (WritingMode::SidewaysRl, false),
12938 (WritingMode::SidewaysLr, false),
12939 ] {
12940 c.writing_mode = Some(wm);
12941 assert_eq!(c.is_vertical(), want, "{wm:?}");
12942 }
12943 c.writing_mode = None;
12944 assert!(!c.is_vertical());
12945 }
12946
12947 #[test]
12948 fn unified_constraints_direction_uses_fallback_unless_the_writing_mode_forces_one() {
12949 let mut c = UnifiedConstraints::default();
12950 assert_eq!(c.direction(BidiDirection::Rtl), BidiDirection::Rtl);
12952 c.writing_mode = Some(WritingMode::HorizontalTb);
12954 assert_eq!(c.direction(BidiDirection::Rtl), BidiDirection::Rtl);
12955 c.writing_mode = Some(WritingMode::VerticalRl);
12957 assert_eq!(c.direction(BidiDirection::Ltr), BidiDirection::Rtl);
12958 }
12959
12960 #[test]
12961 fn unified_constraints_resolved_line_height_uses_the_strut_for_normal() {
12962 let mut c = UnifiedConstraints::default();
12963 assert_eq!(
12964 c.resolved_line_height(),
12965 DEFAULT_STRUT_ASCENT + DEFAULT_STRUT_DESCENT
12966 );
12967 assert_eq!(c.resolved_line_height(), 16.0);
12968
12969 c.line_height = LineHeight::Px(0.0);
12970 assert_eq!(c.resolved_line_height(), 0.0, "an explicit 0 is honoured");
12971
12972 c.line_height = LineHeight::Px(-5.0);
12974 assert_eq!(c.resolved_line_height(), -5.0);
12975 c.line_height = LineHeight::Px(f32::NAN);
12976 assert!(c.resolved_line_height().is_nan());
12977 }
12978
12979 #[test]
12980 fn unified_constraints_partial_eq_is_rounding_tolerant() {
12981 let mut a = UnifiedConstraints::default();
12983 let mut b = UnifiedConstraints::default();
12984 a.strut_ascent = 12.8;
12985 b.strut_ascent = 12.9;
12986 assert_eq!(a, b, "12.8 and 12.9 both round to 13");
12987 b.strut_ascent = 14.0;
12988 assert_ne!(a, b);
12989 }
12990
12991 #[test]
12996 fn text_decoration_from_css_maps_each_variant_exclusively() {
12997 use azul_css::props::style::text::StyleTextDecoration;
12998 let none = TextDecoration::from_css(StyleTextDecoration::None);
12999 assert_eq!(none, TextDecoration::default());
13000 assert!(!none.underline && !none.strikethrough && !none.overline);
13001
13002 let u = TextDecoration::from_css(StyleTextDecoration::Underline);
13003 assert!(u.underline && !u.strikethrough && !u.overline);
13004
13005 let o = TextDecoration::from_css(StyleTextDecoration::Overline);
13006 assert!(!o.underline && !o.strikethrough && o.overline);
13007
13008 let lt = TextDecoration::from_css(StyleTextDecoration::LineThrough);
13009 assert!(!lt.underline && lt.strikethrough && !lt.overline);
13010 }
13011
13012 #[test]
13017 fn inline_border_info_default_has_no_border_and_no_chrome() {
13018 let b = InlineBorderInfo::default();
13019 assert!(!b.has_border());
13020 assert!(!b.has_chrome());
13021 assert_eq!(b.left_inset(), 0.0);
13022 assert_eq!(b.right_inset(), 0.0);
13023 assert_eq!(b.top_inset(), 0.0);
13024 assert_eq!(b.bottom_inset(), 0.0);
13025 }
13026
13027 #[test]
13028 fn inline_border_info_negative_widths_do_not_count_as_a_border() {
13029 let b = InlineBorderInfo {
13030 top: -1.0,
13031 right: -1.0,
13032 bottom: -1.0,
13033 left: -1.0,
13034 ..InlineBorderInfo::default()
13035 };
13036 assert!(!b.has_border(), "the predicate is strictly `> 0.0`");
13037 assert!(!b.has_chrome());
13038 assert_eq!(b.left_inset(), -1.0);
13040 }
13041
13042 #[test]
13043 fn inline_border_info_padding_alone_is_chrome_but_not_a_border() {
13044 let b = InlineBorderInfo {
13045 padding_left: 4.0,
13046 ..InlineBorderInfo::default()
13047 };
13048 assert!(!b.has_border());
13049 assert!(b.has_chrome());
13050 assert_eq!(b.left_inset(), 4.0);
13051 }
13052
13053 #[test]
13054 fn inline_border_info_nan_border_width_is_not_a_border() {
13055 let b = InlineBorderInfo {
13056 top: f32::NAN,
13057 ..InlineBorderInfo::default()
13058 };
13059 assert!(!b.has_border(), "NaN > 0.0 is false");
13060 assert!(b.top_inset().is_nan(), "but the inset still carries the NaN");
13061 }
13062
13063 #[test]
13064 fn inline_border_info_split_insets_swap_edges_in_rtl() {
13065 let base = InlineBorderInfo {
13066 left: 2.0,
13067 right: 3.0,
13068 padding_left: 1.0,
13069 padding_right: 1.0,
13070 ..InlineBorderInfo::default()
13071 };
13072
13073 let ltr_first = InlineBorderInfo {
13075 is_first_fragment: true,
13076 is_last_fragment: false,
13077 ..base
13078 };
13079 assert_eq!(ltr_first.left_inset(), 3.0);
13080 assert_eq!(ltr_first.right_inset(), 0.0);
13081
13082 let ltr_last = InlineBorderInfo {
13083 is_first_fragment: false,
13084 is_last_fragment: true,
13085 ..base
13086 };
13087 assert_eq!(ltr_last.left_inset(), 0.0);
13088 assert_eq!(ltr_last.right_inset(), 4.0);
13089
13090 let rtl_first = InlineBorderInfo {
13092 is_first_fragment: true,
13093 is_last_fragment: false,
13094 is_rtl: true,
13095 ..base
13096 };
13097 assert_eq!(rtl_first.left_inset(), 0.0);
13098 assert_eq!(rtl_first.right_inset(), 4.0);
13099
13100 let rtl_last = InlineBorderInfo {
13101 is_first_fragment: false,
13102 is_last_fragment: true,
13103 is_rtl: true,
13104 ..base
13105 };
13106 assert_eq!(rtl_last.left_inset(), 3.0);
13107 assert_eq!(rtl_last.right_inset(), 0.0);
13108
13109 let middle = InlineBorderInfo {
13111 is_first_fragment: false,
13112 is_last_fragment: false,
13113 ..base
13114 };
13115 assert_eq!(middle.left_inset(), 0.0);
13116 assert_eq!(middle.right_inset(), 0.0);
13117 let tall = InlineBorderInfo {
13119 top: 1.0,
13120 bottom: 2.0,
13121 padding_top: 3.0,
13122 padding_bottom: 4.0,
13123 is_first_fragment: false,
13124 is_last_fragment: false,
13125 ..InlineBorderInfo::default()
13126 };
13127 assert_eq!(tall.top_inset(), 4.0);
13128 assert_eq!(tall.bottom_inset(), 6.0);
13129 }
13130
13131 #[test]
13136 fn style_layout_eq_ignores_render_only_properties() {
13137 let a = StyleProperties::default();
13138 let b = StyleProperties {
13139 color: ColorU {
13140 r: 255,
13141 g: 0,
13142 b: 0,
13143 a: 255,
13144 },
13145 background_color: Some(ColorU::TRANSPARENT),
13146 text_decoration: TextDecoration {
13147 underline: true,
13148 strikethrough: false,
13149 overline: false,
13150 },
13151 border: Some(InlineBorderInfo::default()),
13152 ..StyleProperties::default()
13153 };
13154 assert_ne!(a, b, "the full PartialEq DOES see the colour change");
13155 assert!(
13156 a.layout_eq(&b),
13157 "but layout_eq must ignore colour/decoration/border"
13158 );
13159 assert_eq!(a.layout_hash(), b.layout_hash());
13160 }
13161
13162 #[test]
13163 fn style_layout_eq_sees_sub_pixel_font_size_changes() {
13164 let a = StyleProperties::default();
13165 let b = StyleProperties {
13166 font_size_px: 16.4,
13167 ..StyleProperties::default()
13168 };
13169 assert!(
13170 !a.layout_eq(&b),
13171 "16.0 vs 16.4 must NOT share a shaping-cache entry"
13172 );
13173 }
13174
13175 #[test]
13176 fn style_layout_eq_sees_spacing_and_font_stack_changes() {
13177 let base = StyleProperties::default();
13178
13179 let spaced = StyleProperties {
13180 letter_spacing: Spacing::PxF(0.5),
13181 ..StyleProperties::default()
13182 };
13183 assert!(!base.layout_eq(&spaced));
13184
13185 let worded = StyleProperties {
13186 word_spacing: Spacing::Em(0.1),
13187 ..StyleProperties::default()
13188 };
13189 assert!(!base.layout_eq(&worded));
13190
13191 let other_font = StyleProperties {
13192 font_stack: FontStack::Stack(vec![sel("Arial")]),
13193 ..StyleProperties::default()
13194 };
13195 assert!(!base.layout_eq(&other_font));
13196
13197 let vertical = StyleProperties {
13198 writing_mode: WritingMode::VerticalRl,
13199 ..StyleProperties::default()
13200 };
13201 assert!(!base.layout_eq(&vertical));
13202 }
13203
13204 #[test]
13205 fn style_layout_hash_is_stable_across_repeated_calls() {
13206 let s = StyleProperties::default();
13207 assert_eq!(s.layout_hash(), s.layout_hash());
13208 assert!(s.layout_eq(&StyleProperties::default()));
13209 }
13210
13211 #[test]
13212 fn style_layout_eq_treats_two_nan_font_sizes_as_equal() {
13213 let a = StyleProperties {
13215 font_size_px: f32::NAN,
13216 ..StyleProperties::default()
13217 };
13218 let b = StyleProperties {
13219 font_size_px: f32::NAN,
13220 ..StyleProperties::default()
13221 };
13222 assert!(a.layout_eq(&b));
13223 assert!(!a.layout_eq(&StyleProperties::default()));
13224 }
13225
13226 #[test]
13227 fn style_apply_override_with_an_empty_partial_changes_nothing() {
13228 let base = StyleProperties::default();
13229 let out = base.apply_override(&PartialStyleProperties::default());
13230 assert_eq!(out, base);
13231 }
13232
13233 #[test]
13234 fn style_apply_override_applies_only_the_some_fields() {
13235 let base = StyleProperties::default();
13236 let partial = PartialStyleProperties {
13237 font_size_px: Some(32.0),
13238 letter_spacing: Some(Spacing::PxF(1.5)),
13239 ..PartialStyleProperties::default()
13240 };
13241 let out = base.apply_override(&partial);
13242 assert_eq!(out.font_size_px, 32.0);
13243 assert_eq!(out.letter_spacing, Spacing::PxF(1.5));
13244 assert_eq!(out.word_spacing, base.word_spacing);
13246 assert_eq!(out.tab_size, base.tab_size);
13247 assert_eq!(out.font_stack, base.font_stack);
13248 assert!(!out.layout_eq(&base));
13249 }
13250
13251 #[test]
13252 fn style_apply_override_can_inject_nan_font_size() {
13253 let base = StyleProperties::default();
13254 let partial = PartialStyleProperties {
13255 font_size_px: Some(f32::NAN),
13256 ..PartialStyleProperties::default()
13257 };
13258 let out = base.apply_override(&partial);
13259 assert!(out.font_size_px.is_nan(), "no validation happens here");
13260 }
13261
13262 #[test]
13267 fn classify_character_covers_each_class() {
13268 assert_eq!(classify_character(0x0020), CharacterClass::Space);
13269 assert_eq!(classify_character(0x00A0), CharacterClass::Space);
13270 assert_eq!(classify_character(0x3000), CharacterClass::Space);
13271 assert_eq!(classify_character('.' as u32), CharacterClass::Punctuation);
13272 assert_eq!(classify_character('~' as u32), CharacterClass::Punctuation);
13273 assert_eq!(classify_character('a' as u32), CharacterClass::Letter);
13274 assert_eq!(classify_character(0x4E00), CharacterClass::Ideograph);
13275 assert_eq!(classify_character(0x9FFF), CharacterClass::Ideograph);
13276 assert_eq!(classify_character(0x0301), CharacterClass::Combining);
13277 }
13278
13279 #[test]
13280 fn classify_character_at_u32_extremes_defaults_to_letter() {
13281 assert_eq!(classify_character(0), CharacterClass::Letter);
13282 assert_eq!(classify_character(u32::MAX), CharacterClass::Letter);
13283 assert_eq!(classify_character(0x4DFF), CharacterClass::Letter);
13285 assert_eq!(classify_character(0xA000), CharacterClass::Letter);
13286 }
13287
13288 #[test]
13289 fn get_justification_priority_is_strictly_ordered_space_to_combining() {
13290 let p = |c| get_justification_priority(c);
13291 assert_eq!(p(CharacterClass::Space), 0);
13292 assert_eq!(p(CharacterClass::Combining), 255);
13293 assert!(p(CharacterClass::Space) < p(CharacterClass::Punctuation));
13294 assert!(p(CharacterClass::Punctuation) < p(CharacterClass::Ideograph));
13295 assert!(p(CharacterClass::Ideograph) < p(CharacterClass::Letter));
13296 assert!(p(CharacterClass::Letter) < p(CharacterClass::Symbol));
13297 assert!(p(CharacterClass::Symbol) < p(CharacterClass::Combining));
13298 }
13299
13300 #[test]
13305 fn is_hanging_punctuation_char_only_stops_and_commas() {
13306 assert!(is_hanging_punctuation_char(','));
13307 assert!(is_hanging_punctuation_char('.'));
13308 assert!(is_hanging_punctuation_char('\u{3001}'));
13309 assert!(is_hanging_punctuation_char('\u{FF0E}'));
13310 assert!(!is_hanging_punctuation_char(';'));
13311 assert!(!is_hanging_punctuation_char(' '));
13312 assert!(!is_hanging_punctuation_char('\0'));
13313 assert!(!is_hanging_punctuation_char(char::MAX));
13314 }
13315
13316 #[test]
13317 fn is_word_char_is_alphanumeric_or_underscore() {
13318 assert!(is_word_char('a'));
13319 assert!(is_word_char('Z'));
13320 assert!(is_word_char('9'));
13321 assert!(is_word_char('_'));
13322 assert!(is_word_char('é'), "non-ASCII letters are word chars");
13323 assert!(is_word_char('中'), "ideographs are alphanumeric");
13324 assert!(!is_word_char(' '));
13325 assert!(!is_word_char('-'));
13326 assert!(!is_word_char('.'));
13327 assert!(!is_word_char('\u{00A0}'));
13328 assert!(!is_word_char('\0'));
13329 }
13330
13331 #[test]
13332 fn is_word_separator_char_excludes_tabs_and_fixed_width_spaces() {
13333 assert!(is_word_separator_char(' '));
13334 assert!(is_word_separator_char('\u{00A0}'), "NBSP IS a word separator");
13335 assert!(is_word_separator_char('\u{1680}'));
13336 assert!(is_word_separator_char('\u{202F}'));
13337 assert!(is_word_separator_char('\u{10100}'));
13338
13339 assert!(!is_word_separator_char('\u{2000}'));
13341 assert!(!is_word_separator_char('\u{200A}'));
13342 assert!(!is_word_separator_char('\u{3000}'), "ideographic space excluded");
13343 assert!(!is_word_separator_char('\t'));
13345 assert!(!is_word_separator_char('\n'));
13346 assert!(!is_word_separator_char('.'));
13347 assert!(!is_word_separator_char(char::MAX));
13348 }
13349
13350 #[test]
13351 fn is_cursive_script_char_boundaries() {
13352 assert!(!is_cursive_script_char('\u{05FF}'), "one below Arabic");
13353 assert!(is_cursive_script_char('\u{0600}'), "Arabic block start");
13354 assert!(is_cursive_script_char('\u{06FF}'), "Arabic block end");
13355 assert!(is_cursive_script_char('\u{0700}'), "Syriac");
13356 assert!(is_cursive_script_char('\u{1800}'), "Mongolian");
13357 assert!(is_cursive_script_char('\u{10D00}'), "Hanifi Rohingya (astral)");
13358 assert!(!is_cursive_script_char('a'));
13359 assert!(!is_cursive_script_char('中'));
13360 assert!(!is_cursive_script_char('\0'));
13361 assert!(!is_cursive_script_char(char::MAX));
13362 }
13363
13364 #[test]
13365 fn is_cjk_character_boundaries() {
13366 assert!(is_cjk_character('中')); assert!(is_cjk_character('\u{4E00}'));
13368 assert!(is_cjk_character('\u{9FFF}'));
13369 assert!(is_cjk_character('\u{3040}'), "hiragana block");
13370 assert!(is_cjk_character('\u{30FF}'), "katakana block");
13371 assert!(is_cjk_character('\u{AC00}'), "hangul syllables");
13372 assert!(is_cjk_character('\u{FF01}'), "fullwidth forms");
13373 assert!(!is_cjk_character('\u{4DFF}'), "one below the ideograph block");
13374 assert!(!is_cjk_character('a'));
13375 assert!(!is_cjk_character('\0'));
13376 assert!(!is_cjk_character(char::MAX));
13377 }
13378
13379 #[test]
13380 fn break_control_predicates_are_disjoint() {
13381 assert!(is_break_suppressing_control('\u{200D}'));
13382 assert!(is_break_suppressing_control('\u{2060}'));
13383 assert!(is_break_suppressing_control('\u{FEFF}'));
13384 assert!(!is_break_suppressing_control(' '));
13385 assert!(!is_break_suppressing_control('\u{200B}'));
13386
13387 assert!(is_break_forcing_control('\u{200B}'));
13388 assert!(is_break_forcing_control('\u{2028}'));
13389 assert!(is_break_forcing_control('\u{2029}'));
13390 assert!(!is_break_forcing_control(' '));
13391 assert!(!is_break_forcing_control('\u{200D}'));
13392
13393 for ch in ['\u{200D}', '\u{2060}', '\u{FEFF}', '\u{200B}', '\u{2028}'] {
13394 assert!(
13395 !(is_break_suppressing_control(ch) && is_break_forcing_control(ch)),
13396 "{ch:?} cannot both force and suppress a break"
13397 );
13398 }
13399 }
13400
13401 #[test]
13402 fn is_small_kana_matches_only_the_cj_class() {
13403 assert!(is_small_kana('っ'));
13404 assert!(is_small_kana('ゃ'));
13405 assert!(is_small_kana('ッ'));
13406 assert!(is_small_kana('ー'), "prolonged sound mark is class CJ");
13407 assert!(!is_small_kana('つ'), "the FULL-size kana is not CJ");
13408 assert!(!is_small_kana('中'));
13409 assert!(!is_small_kana('a'));
13410 assert!(!is_small_kana('\0'));
13411 }
13412
13413 #[test]
13414 fn is_cjk_break_allowed_by_strictness_per_level() {
13415 use LineBreakStrictness::{Anywhere, Auto, Loose, Normal, Strict};
13416
13417 for ch in ['っ', '\u{301C}', '\u{2010}', '中'] {
13419 assert!(is_cjk_break_allowed_by_strictness(ch, None, Anywhere), "{ch:?}");
13420 assert!(is_cjk_break_allowed_by_strictness(ch, None, Loose), "{ch:?}");
13421 }
13422
13423 for level in [Normal, Auto] {
13425 assert!(!is_cjk_break_allowed_by_strictness('\u{2010}', None, level));
13426 assert!(!is_cjk_break_allowed_by_strictness('\u{2013}', None, level));
13427 assert!(is_cjk_break_allowed_by_strictness('っ', None, level));
13428 assert!(is_cjk_break_allowed_by_strictness('中', None, level));
13429 }
13430
13431 assert!(!is_cjk_break_allowed_by_strictness('っ', None, Strict));
13433 assert!(!is_cjk_break_allowed_by_strictness('ー', None, Strict));
13434 assert!(!is_cjk_break_allowed_by_strictness('\u{301C}', None, Strict));
13435 assert!(!is_cjk_break_allowed_by_strictness('\u{30A0}', None, Strict));
13436 assert!(is_cjk_break_allowed_by_strictness('中', None, Strict));
13437
13438 assert_eq!(
13440 is_cjk_break_allowed_by_strictness('中', Some('x'), Strict),
13441 is_cjk_break_allowed_by_strictness('中', None, Strict)
13442 );
13443 }
13444
13445 fn plain_glyph(codepoint: char, advance: f32) -> Glyph {
13450 Glyph {
13451 glyph_id: 1,
13452 codepoint,
13453 font_hash: 7,
13454 font_metrics: std_metrics(),
13455 style: style(),
13456 source: GlyphSource::Char,
13457 logical_byte_index: 0,
13458 logical_byte_len: codepoint.len_utf8(),
13459 content_index: 0,
13460 cluster: 0,
13461 advance,
13462 kerning: 0.0,
13463 offset: Point { x: 0.0, y: 0.0 },
13464 vertical_advance: advance,
13465 vertical_origin_y: 0.0,
13466 vertical_bearing: Point { x: 0.0, y: 0.0 },
13467 orientation: GlyphOrientation::Horizontal,
13468 script: Script::Latin,
13469 bidi_level: BidiLevel::new(0),
13470 }
13471 }
13472
13473 #[test]
13474 fn glyph_bounds_is_advance_by_resolved_line_height() {
13475 let g = plain_glyph('a', 9.5);
13476 let b = g.bounds();
13477 assert_eq!(b.x, 0.0);
13478 assert_eq!(b.y, 0.0);
13479 assert_eq!(b.width, 9.5);
13480 approx(b.height, 16.0); }
13482
13483 #[test]
13484 fn glyph_bounds_with_zero_advance_and_zero_upem_does_not_panic() {
13485 let mut g = plain_glyph('a', 0.0);
13486 g.font_metrics = metrics(0, 0.0, 0.0, 0.0);
13487 let b = g.bounds();
13488 assert_eq!(b.width, 0.0);
13489 approx(b.height, 19.2); }
13491
13492 #[test]
13493 fn glyph_whitespace_and_justification_predicates() {
13494 let space = plain_glyph(' ', 4.0);
13495 assert!(space.is_whitespace());
13496 assert_eq!(space.character_class(), CharacterClass::Space);
13497 assert!(!space.can_justify(), "whitespace is never itself justified");
13498 assert_eq!(space.justification_priority(), 0);
13499 assert!(space.break_opportunity_after());
13500
13501 let letter = plain_glyph('a', 8.0);
13502 assert!(!letter.is_whitespace());
13503 assert!(letter.can_justify());
13504 assert_eq!(letter.justification_priority(), 192);
13505 assert!(!letter.break_opportunity_after());
13506
13507 let combining = plain_glyph('\u{0301}', 0.0);
13508 assert!(!combining.is_whitespace());
13509 assert!(!combining.can_justify(), "combining marks are never justified");
13510 assert_eq!(combining.justification_priority(), 255);
13511 }
13512
13513 #[test]
13514 fn glyph_break_opportunity_after_covers_every_hyphen_form() {
13515 assert!(plain_glyph('\u{00AD}', 0.0).break_opportunity_after(), "soft hyphen");
13516 assert!(plain_glyph('\u{002D}', 4.0).break_opportunity_after(), "hyphen-minus");
13517 assert!(plain_glyph('\u{2010}', 4.0).break_opportunity_after(), "U+2010");
13518 assert!(plain_glyph('\t', 8.0).break_opportunity_after(), "tab is whitespace");
13519 assert!(!plain_glyph('\u{2011}', 4.0).break_opportunity_after(), "NON-BREAKING hyphen");
13520 assert!(!plain_glyph('/', 4.0).break_opportunity_after());
13521 }
13522
13523 #[test]
13528 fn shaped_item_as_cluster_only_matches_clusters() {
13529 assert!(cl("a", 8.0).as_cluster().is_some());
13530 assert!(obj(10.0, 10.0, 0.0).as_cluster().is_none());
13531 assert!(brk().as_cluster().is_none());
13532 assert!(tab(8.0, 16.0).as_cluster().is_none());
13533 }
13534
13535 #[test]
13536 fn shaped_item_bounds_of_a_break_is_the_zero_rect() {
13537 assert_eq!(brk().bounds(), Rect::default());
13538 assert_eq!(obj(10.0, 20.0, 0.0).bounds().width, 10.0);
13539 assert_eq!(tab(8.0, 16.0).bounds().height, 16.0);
13540 let c = cl("a", 9.5);
13541 assert_eq!(c.bounds().width, 9.5, "a cluster's width is its advance");
13542 approx(c.bounds().height, 16.0); }
13544
13545 #[test]
13546 fn get_item_measure_sums_advance_and_kerning() {
13547 let st = style();
13548 let mut g1 = shaped_glyph(st.clone(), std_metrics(), 8.0);
13549 g1.kerning = -1.5;
13550 let mut g2 = shaped_glyph(st.clone(), std_metrics(), 8.0);
13551 g2.kerning = 0.5;
13552 let item = make_cluster("ab", 16.0, st, smallvec![g1, g2], gid(0, 0));
13553 assert_eq!(get_item_measure(&item, false), 15.0, "16 + (-1.5) + 0.5");
13554 assert_eq!(
13555 get_item_measure(&item, true),
13556 15.0,
13557 "clusters ignore the is_vertical flag (advance is already axis-relative)"
13558 );
13559 }
13560
13561 #[test]
13562 fn get_item_measure_of_a_break_is_zero_and_objects_switch_axis() {
13563 assert_eq!(get_item_measure(&brk(), false), 0.0);
13564 assert_eq!(get_item_measure(&brk(), true), 0.0);
13565 let o = obj(30.0, 20.0, 0.0);
13566 assert_eq!(get_item_measure(&o, false), 30.0);
13567 assert_eq!(get_item_measure(&o, true), 20.0);
13568 }
13569
13570 #[test]
13571 fn get_item_measure_with_spacing_adds_letter_spacing_but_not_for_cursive() {
13572 let st = styled(|s| s.letter_spacing = Spacing::PxF(2.0));
13573 let latin = cl_styled("a", 10.0, st.clone());
13574 assert_eq!(get_item_measure(&latin, false), 10.0);
13575 assert_eq!(get_item_measure_with_spacing(&latin, false), 12.0);
13576
13577 let arabic = cl_styled("\u{0627}", 10.0, st);
13579 assert_eq!(
13580 get_item_measure_with_spacing(&arabic, false),
13581 10.0,
13582 "letter-spacing is suppressed for cursive scripts (CSS Text 3 App. D)"
13583 );
13584 }
13585
13586 #[test]
13587 fn get_item_measure_with_spacing_adds_word_spacing_only_on_separators() {
13588 let st = styled(|s| {
13589 s.word_spacing = Spacing::PxF(5.0);
13590 s.letter_spacing = Spacing::PxF(1.0);
13591 });
13592 let space = cl_styled(" ", 4.0, st.clone());
13593 assert_eq!(
13594 get_item_measure_with_spacing(&space, false),
13595 10.0,
13596 "4 + letter(1) + word(5)"
13597 );
13598 let letter = cl_styled("a", 8.0, st);
13599 assert_eq!(
13600 get_item_measure_with_spacing(&letter, false),
13601 9.0,
13602 "no word-spacing on a non-separator"
13603 );
13604 assert_eq!(get_item_measure_with_spacing(&brk(), false), 0.0);
13606 }
13607
13608 #[test]
13609 fn is_collapsible_whitespace_is_vacuously_true_for_an_empty_cluster() {
13610 assert!(is_collapsible_whitespace(&cl(" ", 4.0)));
13611 assert!(is_collapsible_whitespace(&cl("\t", 8.0)));
13612 assert!(is_collapsible_whitespace(&cl("\u{1680}", 4.0)));
13613 assert!(is_collapsible_whitespace(&cl(" \t ", 16.0)));
13614 assert!(!is_collapsible_whitespace(&cl("\n", 0.0)), "newline is NOT collapsible here");
13615 assert!(!is_collapsible_whitespace(&cl("a", 8.0)));
13616 assert!(!is_collapsible_whitespace(&cl("a ", 12.0)), "all() — mixed is false");
13617 assert!(!is_collapsible_whitespace(&obj(1.0, 1.0, 0.0)));
13618 assert!(
13621 is_collapsible_whitespace(&cl("", 0.0)),
13622 "an empty cluster counts as collapsible whitespace"
13623 );
13624 }
13625
13626 #[test]
13627 fn is_word_separator_and_zero_width_space_on_items() {
13628 assert!(is_word_separator(&cl(" ", 4.0)));
13629 assert!(is_word_separator(&cl("a b", 20.0)), "any() — one space suffices");
13630 assert!(!is_word_separator(&cl("", 0.0)), "any() on empty is false");
13631 assert!(!is_word_separator(&cl("\u{3000}", 16.0)));
13632 assert!(!is_word_separator(&brk()));
13633 assert!(!is_word_separator(&obj(1.0, 1.0, 0.0)));
13634
13635 assert!(is_zero_width_space(&cl("\u{200B}", 0.0)));
13636 assert!(is_zero_width_space(&cl("a\u{200B}", 8.0)), "contains(), not equals()");
13637 assert!(!is_zero_width_space(&cl(" ", 4.0)));
13638 assert!(!is_zero_width_space(&obj(1.0, 1.0, 0.0)));
13639 }
13640
13641 #[test]
13642 fn can_justify_after_rejects_objects_empty_clusters_and_combining_marks() {
13643 assert!(can_justify_after(&cl("a", 8.0)));
13644 assert!(!can_justify_after(&cl(" ", 4.0)));
13645 assert!(!can_justify_after(&cl("a\u{0301}", 8.0)), "trailing combining mark");
13646 assert!(!can_justify_after(&cl("", 0.0)), "no last char → false");
13647 assert!(
13648 !can_justify_after(&obj(10.0, 10.0, 0.0)),
13649 "CSS 2.2 §9.4.2: never stretch after an atomic inline"
13650 );
13651 assert!(!can_justify_after(&brk()));
13652 }
13653
13654 #[test]
13655 fn is_hanging_punctuation_requires_a_single_glyph_cluster() {
13656 assert!(is_hanging_punctuation(&cl(".", 4.0)));
13657 assert!(is_hanging_punctuation(&cl(",", 4.0)));
13658 assert!(!is_hanging_punctuation(&cl("a", 8.0)));
13659 assert!(!is_hanging_punctuation(&cl("", 0.0)), "no first char");
13660 assert!(!is_hanging_punctuation(&obj(1.0, 1.0, 0.0)));
13661
13662 let st = style();
13664 let g1 = shaped_glyph(st.clone(), std_metrics(), 4.0);
13665 let g2 = shaped_glyph(st.clone(), std_metrics(), 4.0);
13666 let two = make_cluster(".", 8.0, st, smallvec![g1, g2], gid(0, 0));
13667 assert!(!is_hanging_punctuation(&two));
13668 }
13669
13670 #[test]
13671 fn cluster_script_predicates() {
13672 let arabic = cl("\u{0627}", 10.0);
13673 let latin = cl("a", 8.0);
13674 let cjk = cl("中", 16.0);
13675
13676 assert!(is_cursive_script_cluster(arabic.as_cluster().unwrap()));
13677 assert!(!is_cursive_script_cluster(latin.as_cluster().unwrap()));
13678 assert!(
13679 !is_cursive_script_cluster(cl("", 0.0).as_cluster().unwrap()),
13680 "empty cluster has no first char"
13681 );
13682
13683 assert!(is_cjk_cluster(cjk.as_cluster().unwrap()));
13684 assert!(!is_cjk_cluster(latin.as_cluster().unwrap()));
13685
13686 assert!(
13688 !is_arabic_cluster(arabic.as_cluster().unwrap()),
13689 "the fixture's glyph carries Script::Latin, so the text alone is not enough"
13690 );
13691 let st = style();
13692 let mut g = shaped_glyph(st.clone(), std_metrics(), 10.0);
13693 g.script = Script::Arabic;
13694 let real_arabic = make_cluster("\u{0627}", 10.0, st, smallvec![g], gid(0, 0));
13695 assert!(is_arabic_cluster(real_arabic.as_cluster().unwrap()));
13696 }
13697
13698 #[test]
13699 fn cluster_is_word_boundary_for_punctuation_and_whitespace() {
13700 assert!(cluster_is_word_boundary(cl(" ", 4.0).as_cluster().unwrap()));
13701 assert!(cluster_is_word_boundary(cl(".", 4.0).as_cluster().unwrap()));
13702 assert!(cluster_is_word_boundary(cl("", 0.0).as_cluster().unwrap()), "vacuous");
13703 assert!(!cluster_is_word_boundary(cl("a", 8.0).as_cluster().unwrap()));
13704 assert!(!cluster_is_word_boundary(cl("_", 8.0).as_cluster().unwrap()));
13705 }
13706
13707 #[test]
13708 fn get_baseline_for_item_only_defined_for_clusters_and_boxes() {
13709 assert_eq!(get_baseline_for_item(&brk()), None);
13710 assert_eq!(get_baseline_for_item(&tab(8.0, 16.0)), None);
13711 assert_eq!(get_baseline_for_item(&obj(10.0, 20.0, 3.0)), Some(3.0));
13712 approx(
13714 get_baseline_for_item(&cl("a", 8.0)).expect("a glyph-bearing cluster has a baseline"),
13715 12.8,
13716 );
13717 assert_eq!(
13718 get_baseline_for_item(&cl_no_glyphs("", 0.0)),
13719 None,
13720 "a glyph-less cluster has no baseline"
13721 );
13722 }
13723
13724 #[test]
13725 fn get_item_vertical_metrics_approx_for_every_variant() {
13726 let (a, d) = get_item_vertical_metrics_approx(&cl("a", 8.0));
13728 approx(a, 12.8);
13729 approx(d, 3.2);
13730
13731 let (a, d) = get_item_vertical_metrics_approx(&cl_no_glyphs("", 0.0));
13733 approx(a, 19.2 * FALLBACK_ASCENT_RATIO);
13734 approx(d, 19.2 * FALLBACK_DESCENT_RATIO);
13735
13736 assert_eq!(get_item_vertical_metrics_approx(&obj(10.0, 20.0, 5.0)), (20.0, 0.0));
13738 assert_eq!(get_item_vertical_metrics_approx(&brk()), (0.0, 0.0));
13740 let (a, d) = get_item_vertical_metrics_approx(&tab(8.0, 10.0));
13742 approx(a, 8.0);
13743 approx(d, 2.0);
13744 }
13745
13746 #[test]
13747 fn get_item_vertical_metrics_approx_skips_zero_upem_glyphs() {
13748 let st = style();
13749 let g = shaped_glyph(st.clone(), metrics(0, 800.0, -200.0, 0.0), 8.0);
13750 let item = make_cluster("a", 8.0, st, smallvec![g], gid(0, 0));
13751 assert_eq!(
13752 get_item_vertical_metrics_approx(&item),
13753 (0.0, 0.0),
13754 "a zero-upem glyph is skipped rather than producing inf/NaN metrics"
13755 );
13756 }
13757
13758 #[test]
13759 fn get_item_vertical_metrics_uses_the_strut_for_glyphless_clusters() {
13760 let c = UnifiedConstraints::default();
13761 let (a, d) = get_item_vertical_metrics(&cl_no_glyphs("", 0.0), &c);
13762 approx(a, DEFAULT_STRUT_ASCENT + 1.6);
13764 approx(d, DEFAULT_STRUT_DESCENT + 1.6);
13765
13766 assert_eq!(get_item_vertical_metrics(&brk(), &c), (0.0, 0.0));
13767 let (a, d) = get_item_vertical_metrics(&obj(10.0, 10.0, 30.0), &c);
13769 assert_eq!(a, 0.0, "baseline_offset > height must clamp the ascent at 0");
13770 assert_eq!(d, 30.0);
13771 }
13772
13773 #[test]
13774 fn get_item_vertical_align_only_for_objects_with_image_or_shape_content() {
13775 assert_eq!(get_item_vertical_align(&cl("a", 8.0)), None);
13776 assert_eq!(get_item_vertical_align(&brk()), None);
13777 assert_eq!(get_item_vertical_align(&obj(1.0, 1.0, 0.0)), None);
13779
13780 let img = ShapedItem::Object {
13781 source: ci(0, 0),
13782 bounds: Rect::default(),
13783 baseline_offset: 0.0,
13784 content: InlineContent::Image(InlineImage {
13785 source: ImageSource::Placeholder(Size::new(10.0, 10.0)),
13786 intrinsic_size: Size::new(10.0, 10.0),
13787 display_size: None,
13788 baseline_offset: 0.0,
13789 alignment: VerticalAlign::Top,
13790 object_fit: ObjectFit::Fill,
13791 }),
13792 };
13793 assert_eq!(get_item_vertical_align(&img), Some(VerticalAlign::Top));
13794 }
13795
13796 #[test]
13801 fn no_break_space_is_a_word_separator_but_never_a_break_opportunity() {
13802 let nbsp = cl("\u{00A0}", 4.0);
13803 assert!(is_word_separator( ), "NBSP participates in word-spacing");
13804 assert!(
13805 !is_break_opportunity( ),
13806 "...but must NOT offer a soft wrap (10\\u{{00A0}}km must not wrap)"
13807 );
13808 assert!(!is_break_opportunity_with_word_break(
13809  ,
13810 WordBreak::BreakAll,
13811 Hyphens::Auto
13812 ));
13813 for ch in ['\u{202F}', '\u{2060}', '\u{FEFF}'] {
13815 let item = cl(&ch.to_string(), 4.0);
13816 assert!(
13817 !is_break_opportunity_with_word_break(&item, WordBreak::BreakAll, Hyphens::Auto),
13818 "{ch:?} must suppress breaks"
13819 );
13820 }
13821 }
13822
13823 #[test]
13824 fn zero_width_space_breaks_even_under_keep_all() {
13825 let zwsp = cl("\u{200B}", 0.0);
13826 assert!(is_break_opportunity(&zwsp));
13827 for wb in [WordBreak::Normal, WordBreak::BreakAll, WordBreak::KeepAll] {
13828 assert!(
13829 is_break_opportunity_with_word_break(&zwsp, wb, Hyphens::None),
13830 "ZWSP must always break ({wb:?})"
13831 );
13832 }
13833 }
13834
13835 #[test]
13836 fn word_break_modes_change_cjk_break_opportunities() {
13837 let cjk = cl("中", 16.0);
13838 assert!(is_break_opportunity_with_word_break(&cjk, WordBreak::Normal, Hyphens::Manual));
13839 assert!(is_break_opportunity_with_word_break(&cjk, WordBreak::BreakAll, Hyphens::Manual));
13840 assert!(
13841 !is_break_opportunity_with_word_break(&cjk, WordBreak::KeepAll, Hyphens::Manual),
13842 "keep-all suppresses inter-ideograph breaks"
13843 );
13844
13845 let latin = cl("a", 8.0);
13846 assert!(!is_break_opportunity_with_word_break(&latin, WordBreak::Normal, Hyphens::Manual));
13847 assert!(
13848 is_break_opportunity_with_word_break(&latin, WordBreak::BreakAll, Hyphens::Manual),
13849 "break-all makes every cluster breakable"
13850 );
13851 assert!(!is_break_opportunity_with_word_break(&latin, WordBreak::KeepAll, Hyphens::Manual));
13852 }
13853
13854 #[test]
13855 fn soft_hyphen_break_depends_on_the_hyphens_property() {
13856 let shy = cl("\u{00AD}", 0.0);
13857 assert!(!is_break_opportunity_with_word_break(
13858 ­,
13859 WordBreak::Normal,
13860 Hyphens::None
13861 ));
13862 assert!(is_break_opportunity_with_word_break(
13863 ­,
13864 WordBreak::Normal,
13865 Hyphens::Manual
13866 ));
13867 assert!(is_break_opportunity_with_word_break(
13868 ­,
13869 WordBreak::Normal,
13870 Hyphens::Auto
13871 ));
13872 }
13873
13874 #[test]
13875 fn trailing_hyphen_and_slash_always_break_regardless_of_hyphens() {
13876 for text in ["co-", "co\u{2010}", "a/"] {
13877 let item = cl(text, 10.0);
13878 assert!(
13879 is_break_opportunity_with_word_break(&item, WordBreak::KeepAll, Hyphens::None),
13880 "{text:?} must offer a break after it even with hyphens:none"
13881 );
13882 }
13883 assert!(!is_break_opportunity_with_word_break(
13885 &cl("-a", 10.0),
13886 WordBreak::Normal,
13887 Hyphens::None
13888 ));
13889 }
13890
13891 #[test]
13892 fn atomic_inlines_are_break_opportunities_but_breaks_and_tabs_differ() {
13893 assert!(is_break_opportunity(&obj(10.0, 10.0, 0.0)), "CSS Text 3 §5.1");
13894 assert!(is_break_opportunity(&brk()));
13895 assert!(!is_break_opportunity(&tab(8.0, 16.0)), "a Tab is not itself a wrap point");
13896 assert!(is_break_opportunity(&cl(" ", 4.0)));
13897 assert!(!is_break_opportunity(&cl("a", 8.0)));
13898 }
13899
13900 #[test]
13905 fn merge_segments_of_zero_or_one_segment_is_identity() {
13906 assert!(merge_segments(Vec::new()).is_empty());
13907 let one = vec![LineSegment {
13908 start_x: 5.0,
13909 width: 3.0,
13910 priority: 0,
13911 }];
13912 let out = merge_segments(one);
13913 assert_eq!(out.len(), 1);
13914 assert_eq!(out[0].start_x, 5.0);
13915 }
13916
13917 #[test]
13918 fn merge_segments_joins_overlapping_and_touching_spans() {
13919 let segs = vec![
13920 LineSegment {
13921 start_x: 0.0,
13922 width: 10.0,
13923 priority: 0,
13924 },
13925 LineSegment {
13926 start_x: 5.0,
13927 width: 10.0,
13928 priority: 0,
13929 }, LineSegment {
13931 start_x: 15.0,
13932 width: 5.0,
13933 priority: 0,
13934 }, LineSegment {
13936 start_x: 100.0,
13937 width: 5.0,
13938 priority: 0,
13939 }, ];
13941 let out = merge_segments(segs);
13942 assert_eq!(out.len(), 2, "three touching spans collapse into one");
13943 assert_eq!(out[0].start_x, 0.0);
13944 assert_eq!(out[0].width, 20.0);
13945 assert_eq!(out[1].start_x, 100.0);
13946 }
13947
13948 #[test]
13949 fn merge_segments_sorts_unordered_input() {
13950 let segs = vec![
13951 LineSegment {
13952 start_x: 50.0,
13953 width: 5.0,
13954 priority: 0,
13955 },
13956 LineSegment {
13957 start_x: 0.0,
13958 width: 5.0,
13959 priority: 0,
13960 },
13961 ];
13962 let out = merge_segments(segs);
13963 assert_eq!(out.len(), 2);
13964 assert_eq!(out[0].start_x, 0.0);
13965 assert_eq!(out[1].start_x, 50.0);
13966 }
13967
13968 #[test]
13969 fn merge_segments_is_nan_tolerant() {
13970 let segs = vec![
13974 LineSegment {
13975 start_x: 0.0,
13976 width: 10.0,
13977 priority: 0,
13978 },
13979 LineSegment {
13980 start_x: f32::NAN,
13981 width: 10.0,
13982 priority: 0,
13983 },
13984 ];
13985 let out = merge_segments(segs);
13986 assert!(!out.is_empty(), "NaN input must not abort the merge");
13987 }
13988
13989 #[test]
13990 fn polygon_line_intersection_needs_at_least_three_points() {
13991 assert!(polygon_line_intersection(&[], 0.0, 1.0).is_empty());
13992 assert!(polygon_line_intersection(&[Point { x: 0.0, y: 0.0 }], 0.0, 1.0).is_empty());
13993 assert!(polygon_line_intersection(
13994 &[Point { x: 0.0, y: 0.0 }, Point { x: 1.0, y: 1.0 }],
13995 0.0,
13996 1.0
13997 )
13998 .is_empty());
13999 }
14000
14001 #[test]
14002 fn polygon_line_intersection_narrows_across_a_triangle() {
14003 let tri = [
14005 Point { x: 0.0, y: 0.0 },
14006 Point { x: 100.0, y: 0.0 },
14007 Point { x: 0.0, y: 100.0 },
14008 ];
14009 let top = polygon_line_intersection(&tri, 10.0, 1.0);
14010 let bot = polygon_line_intersection(&tri, 80.0, 1.0);
14011 assert_eq!(top.len(), 1);
14012 assert_eq!(bot.len(), 1);
14013 assert!(
14014 top[0].width > bot[0].width,
14015 "the band must narrow with y ({} !> {})",
14016 top[0].width,
14017 bot[0].width
14018 );
14019 assert!((top[0].width - 89.5).abs() < 1.0);
14020 }
14021
14022 #[test]
14023 fn polygon_line_intersection_outside_the_shape_and_on_nan_scanlines_is_empty() {
14024 let tri = [
14025 Point { x: 0.0, y: 0.0 },
14026 Point { x: 100.0, y: 0.0 },
14027 Point { x: 0.0, y: 100.0 },
14028 ];
14029 assert!(
14030 polygon_line_intersection(&tri, 500.0, 1.0).is_empty(),
14031 "a scanline below the shape yields no spans"
14032 );
14033 assert!(
14034 polygon_line_intersection(&tri, f32::NAN, 1.0).is_empty(),
14035 "a NaN scanline must not panic — every crossing test is false"
14036 );
14037 assert!(polygon_line_intersection(&tri, f32::INFINITY, 1.0).is_empty());
14038 }
14039
14040 #[test]
14041 fn polygon_line_intersection_of_a_degenerate_flat_polygon_is_empty() {
14042 let flat = [
14044 Point { x: 0.0, y: 5.0 },
14045 Point { x: 10.0, y: 5.0 },
14046 Point { x: 20.0, y: 5.0 },
14047 ];
14048 assert!(polygon_line_intersection(&flat, 4.5, 1.0).is_empty());
14049 }
14050
14051 #[test]
14052 fn path_segments_line_intersection_on_empty_and_degenerate_input() {
14053 assert!(path_segments_line_intersection(&[], 0.0, 1.0).is_empty());
14054 assert!(path_segments_line_intersection(
14056 &[PathSegment::MoveTo(Point { x: 0.0, y: 0.0 })],
14057 0.0,
14058 1.0
14059 )
14060 .is_empty());
14061 }
14062
14063 #[test]
14064 fn path_segments_line_intersection_of_a_square() {
14065 let sq = vec![
14066 PathSegment::MoveTo(Point { x: 0.0, y: 0.0 }),
14067 PathSegment::LineTo(Point { x: 100.0, y: 0.0 }),
14068 PathSegment::LineTo(Point {
14069 x: 100.0,
14070 y: 100.0,
14071 }),
14072 PathSegment::LineTo(Point { x: 0.0, y: 100.0 }),
14073 PathSegment::Close,
14074 ];
14075 let spans = path_segments_line_intersection(&sq, 50.0, 1.0);
14076 assert_eq!(spans.len(), 1);
14077 assert!((spans[0].0 - 0.0).abs() < 0.01);
14078 assert!((spans[0].1 - 100.0).abs() < 0.01);
14079 assert!(path_segments_line_intersection(&sq, 500.0, 1.0).is_empty());
14081 }
14082
14083 #[test]
14084 fn get_shape_horizontal_spans_rectangle_only_when_the_line_box_overlaps() {
14085 let r = ShapeBoundary::Rectangle(Rect {
14086 x: 10.0,
14087 y: 20.0,
14088 width: 30.0,
14089 height: 40.0,
14090 });
14091 assert_eq!(get_shape_horizontal_spans(&r, 30.0, 10.0), vec![(10.0, 40.0)]);
14092 assert!(get_shape_horizontal_spans(&r, 0.0, 10.0).is_empty(), "above");
14093 assert!(get_shape_horizontal_spans(&r, 100.0, 10.0).is_empty(), "below");
14094 assert!(get_shape_horizontal_spans(&r, 10.0, 10.0).is_empty());
14096 let flat = ShapeBoundary::Rectangle(Rect {
14098 x: 0.0,
14099 y: 0.0,
14100 width: 10.0,
14101 height: 0.0,
14102 });
14103 assert!(get_shape_horizontal_spans(&flat, 0.0, 10.0).is_empty());
14104 }
14105
14106 #[test]
14107 fn get_shape_horizontal_spans_circle_edges_and_zero_radius() {
14108 let c = ShapeBoundary::Circle {
14109 center: Point { x: 50.0, y: 50.0 },
14110 radius: 10.0,
14111 };
14112 let mid = get_shape_horizontal_spans(&c, 49.5, 1.0); assert_eq!(mid.len(), 1);
14114 assert!((mid[0].0 - 40.0).abs() < 0.01);
14115 assert!((mid[0].1 - 60.0).abs() < 0.01);
14116 assert!(get_shape_horizontal_spans(&c, 1000.0, 1.0).is_empty());
14117
14118 let dot = ShapeBoundary::Circle {
14120 center: Point { x: 5.0, y: 5.0 },
14121 radius: 0.0,
14122 };
14123 let spans = get_shape_horizontal_spans(&dot, 4.5, 1.0);
14124 assert_eq!(spans, vec![(5.0, 5.0)], "degenerate but not a panic");
14125 }
14126
14127 #[test]
14128 fn get_shape_horizontal_spans_ellipse_with_zero_radii_is_empty_not_a_panic() {
14129 let e = ShapeBoundary::Ellipse {
14131 center: Point { x: 0.0, y: 0.0 },
14132 radii: Size::zero(),
14133 };
14134 assert!(
14135 get_shape_horizontal_spans(&e, 0.0, 1.0).is_empty(),
14136 "a zero-sized ellipse divides by zero but must not panic"
14137 );
14138 }
14139
14140 #[test]
14141 fn get_shape_horizontal_spans_polygon_delegates_to_the_scanline() {
14142 let p = ShapeBoundary::Polygon {
14143 points: vec![
14144 Point { x: 0.0, y: 0.0 },
14145 Point { x: 100.0, y: 0.0 },
14146 Point { x: 0.0, y: 100.0 },
14147 ],
14148 };
14149 let spans = get_shape_horizontal_spans(&p, 10.0, 1.0);
14150 assert_eq!(spans.len(), 1);
14151 assert!(spans[0].1 > spans[0].0);
14152 }
14153
14154 #[test]
14159 fn extract_line_breaks_of_no_items_is_empty_but_keeps_the_width() {
14160 let lb = extract_line_breaks(&[], 640.0);
14161 assert!(lb.line_ranges.is_empty());
14162 assert!(lb.line_widths.is_empty());
14163 assert_eq!(lb.available_width, 640.0);
14164 let nan = extract_line_breaks(&[], f32::NAN);
14166 assert!(nan.available_width.is_nan());
14167 }
14168
14169 #[test]
14170 fn extract_line_breaks_groups_items_by_line_index() {
14171 let items = vec![
14172 pos(cl("a", 10.0), 0.0, 0.0, 0),
14173 pos(cl("b", 10.0), 10.0, 0.0, 0),
14174 pos(cl("c", 10.0), 0.0, 20.0, 1),
14175 ];
14176 let lb = extract_line_breaks(&items, 100.0);
14177 assert_eq!(lb.line_ranges, vec![(0, 2), (2, 3)]);
14178 assert_eq!(lb.line_widths, vec![20.0, 10.0]);
14179 assert_eq!(lb.line_ranges.len(), lb.line_widths.len());
14180 }
14181
14182 #[test]
14183 fn extract_line_breaks_splits_on_every_line_index_change_even_going_backwards() {
14184 let items = vec![
14187 pos(cl("a", 10.0), 0.0, 0.0, 0),
14188 pos(cl("b", 10.0), 0.0, 20.0, 1),
14189 pos(cl("c", 10.0), 0.0, 0.0, 0),
14190 ];
14191 let lb = extract_line_breaks(&items, 100.0);
14192 assert_eq!(lb.line_ranges.len(), 3);
14193 assert_eq!(lb.line_widths, vec![10.0, 10.0, 10.0]);
14194 }
14195
14196 #[test]
14197 fn try_incremental_relayout_no_dirty_items_is_a_glyph_swap() {
14198 let lb = CachedLineBreaks {
14199 line_ranges: vec![(0, 2)],
14200 line_widths: vec![20.0],
14201 available_width: 100.0,
14202 };
14203 assert!(matches!(
14204 try_incremental_relayout(&[], &[10.0, 10.0], &[10.0, 10.0], &lb),
14205 IncrementalRelayoutResult::GlyphSwap
14206 ));
14207 }
14208
14209 #[test]
14210 fn try_incremental_relayout_out_of_range_dirty_index_falls_back_to_full() {
14211 let lb = CachedLineBreaks {
14212 line_ranges: vec![(0, 2)],
14213 line_widths: vec![20.0],
14214 available_width: 100.0,
14215 };
14216 assert!(matches!(
14217 try_incremental_relayout(&[99], &[10.0, 10.0], &[10.0, 10.0], &lb),
14218 IncrementalRelayoutResult::FullRelayout
14219 ));
14220 assert!(
14221 matches!(
14222 try_incremental_relayout(&[usize::MAX], &[10.0], &[10.0], &lb),
14223 IncrementalRelayoutResult::FullRelayout
14224 ),
14225 "usize::MAX must not index-panic"
14226 );
14227 assert!(matches!(
14229 try_incremental_relayout(&[1], &[10.0, 10.0], &[10.0], &lb),
14230 IncrementalRelayoutResult::FullRelayout
14231 ));
14232 }
14233
14234 #[test]
14235 fn try_incremental_relayout_same_width_is_a_glyph_swap() {
14236 let lb = CachedLineBreaks {
14237 line_ranges: vec![(0, 2)],
14238 line_widths: vec![20.0],
14239 available_width: 100.0,
14240 };
14241 assert!(matches!(
14243 try_incremental_relayout(&[0], &[10.0, 10.0], &[10.0005, 10.0], &lb),
14244 IncrementalRelayoutResult::GlyphSwap
14245 ));
14246 }
14247
14248 #[test]
14249 fn try_incremental_relayout_shifts_when_it_still_fits_and_reflows_when_it_does_not() {
14250 let lb = CachedLineBreaks {
14251 line_ranges: vec![(0, 2), (2, 4)],
14252 line_widths: vec![20.0, 20.0],
14253 available_width: 100.0,
14254 };
14255 let old = [10.0, 10.0, 10.0, 10.0];
14256
14257 let grew = [10.0, 30.0, 10.0, 10.0]; match try_incremental_relayout(&[1], &old, &grew, &lb) {
14259 IncrementalRelayoutResult::LineShift {
14260 affected_item,
14261 delta,
14262 } => {
14263 assert_eq!(affected_item, 1);
14264 assert_eq!(delta, 20.0);
14265 }
14266 other => panic!("expected LineShift, got {other:?}"),
14267 }
14268
14269 let exploded = [10.0, 10.0, 10.0, 500.0]; match try_incremental_relayout(&[3], &old, &exploded, &lb) {
14271 IncrementalRelayoutResult::PartialReflow { reflow_from_line } => {
14272 assert_eq!(reflow_from_line, 1);
14273 }
14274 other => panic!("expected PartialReflow, got {other:?}"),
14275 }
14276 }
14277
14278 #[test]
14279 fn try_incremental_relayout_dirty_item_outside_every_line_range_is_a_full_relayout() {
14280 let lb = CachedLineBreaks {
14281 line_ranges: vec![(0, 1)],
14282 line_widths: vec![10.0],
14283 available_width: 100.0,
14284 };
14285 assert!(matches!(
14287 try_incremental_relayout(&[1], &[10.0, 10.0], &[10.0, 50.0], &lb),
14288 IncrementalRelayoutResult::FullRelayout
14289 ));
14290 }
14291
14292 #[test]
14293 fn try_incremental_relayout_with_nan_advances_reflows_rather_than_shifting() {
14294 let lb = CachedLineBreaks {
14295 line_ranges: vec![(0, 1)],
14296 line_widths: vec![10.0],
14297 available_width: 100.0,
14298 };
14299 match try_incremental_relayout(&[0], &[10.0], &[f32::NAN], &lb) {
14302 IncrementalRelayoutResult::PartialReflow { reflow_from_line } => {
14303 assert_eq!(reflow_from_line, 0);
14304 }
14305 other => panic!("NaN advance should reflow, got {other:?}"),
14306 }
14307 }
14308
14309 #[test]
14314 fn text_cache_memory_report_total_bytes_sums_only_the_byte_fields() {
14315 let r = TextCacheMemoryReport::default();
14316 assert_eq!(r.total_bytes(), 0);
14317
14318 let full = TextCacheMemoryReport {
14319 logical_items_entries: 1_000_000, logical_items_bytes: 1,
14321 visual_items_entries: 1_000_000, visual_items_bytes: 2,
14323 shaped_items_entries: 1_000_000, shaped_items_bytes: 4,
14325 shaped_glyph_bytes: 8,
14326 shaped_cluster_text_bytes: 16,
14327 per_item_shaped_entries: 1_000_000, per_item_shaped_bytes: 32,
14329 };
14330 assert_eq!(full.total_bytes(), 63, "1+2+4+8+16+32");
14331 }
14332
14333 #[test]
14334 fn text_shaping_cache_new_is_empty_and_reports_zero_bytes() {
14335 let c = TextShapingCache::new();
14336 let r = c.memory_report();
14337 assert_eq!(r.total_bytes(), 0);
14338 assert_eq!(r.logical_items_entries, 0);
14339 assert_eq!(r.per_item_shaped_entries, 0);
14340 assert_eq!(c.generation, 0);
14341 let d = TextShapingCache::default();
14343 assert_eq!(d.memory_report().total_bytes(), 0);
14344 }
14345
14346 #[test]
14347 fn text_shaping_cache_begin_generation_is_idempotent_on_an_empty_cache() {
14348 let mut c = TextShapingCache::new();
14349 for expect in 1..=5_u64 {
14350 c.begin_generation();
14351 assert_eq!(c.generation, expect);
14352 }
14353 assert!(c.per_item_accessed.is_empty());
14354 assert!(c.per_item_shaped.is_empty());
14355 }
14356
14357 #[test]
14358 fn text_shaping_cache_begin_generation_evicts_unaccessed_per_item_entries() {
14359 let mut c = TextShapingCache::new();
14360 c.per_item_shaped.insert(
14361 1,
14362 Arc::new(PerItemShapedEntry {
14363 clusters: vec![cl("a", 8.0)],
14364 total_advance: 8.0,
14365 }),
14366 );
14367 c.per_item_shaped.insert(
14368 2,
14369 Arc::new(PerItemShapedEntry {
14370 clusters: Vec::new(),
14371 total_advance: 0.0,
14372 }),
14373 );
14374 c.begin_generation();
14376 assert_eq!(c.per_item_shaped.len(), 2, "gen 0 never evicts");
14377
14378 c.per_item_accessed.insert(1);
14380 c.begin_generation();
14381 assert_eq!(c.per_item_shaped.len(), 1);
14382 assert!(c.per_item_shaped.contains_key(&1));
14383
14384 c.begin_generation();
14386 assert_eq!(
14387 c.per_item_shaped.len(),
14388 1,
14389 "an empty access-set must not wipe the cache"
14390 );
14391 }
14392
14393 #[test]
14394 fn use_old_layout_accepts_a_render_only_change_and_rejects_layout_changes() {
14395 let c = UnifiedConstraints::default();
14396 let red = styled(|s| {
14397 s.color = ColorU {
14398 r: 255,
14399 g: 0,
14400 b: 0,
14401 a: 255,
14402 };
14403 });
14404 let old = [text_content("hi", style())];
14405 let new_colour = [text_content("hi", red)];
14406 assert!(
14407 TextShapingCache::use_old_layout(&c, &c, &old, &new_colour),
14408 "a colour-only change must reuse the cached layout"
14409 );
14410
14411 let new_text = [text_content("ho", style())];
14413 assert!(!TextShapingCache::use_old_layout(&c, &c, &old, &new_text));
14414
14415 let bigger = [text_content("hi", styled(|s| s.font_size_px = 32.0))];
14417 assert!(!TextShapingCache::use_old_layout(&c, &c, &old, &bigger));
14418
14419 let c2 = UnifiedConstraints {
14421 available_width: AvailableSpace::Definite(100.0),
14422 ..Default::default()
14423 };
14424 assert!(!TextShapingCache::use_old_layout(&c, &c2, &old, &old));
14425 }
14426
14427 #[test]
14428 fn use_old_layout_on_empty_content_and_length_or_variant_mismatch() {
14429 let c = UnifiedConstraints::default();
14430 assert!(
14431 TextShapingCache::use_old_layout(&c, &c, &[], &[]),
14432 "empty vs empty is trivially reusable"
14433 );
14434 let one = [text_content("a", style())];
14435 assert!(!TextShapingCache::use_old_layout(&c, &c, &[], &one));
14436 assert!(!TextShapingCache::use_old_layout(&c, &c, &one, &[]));
14437
14438 let space = [InlineContent::Space(InlineSpace {
14440 width: 4.0,
14441 is_breaking: true,
14442 is_stretchy: true,
14443 })];
14444 assert!(!TextShapingCache::use_old_layout(&c, &c, &one, &space));
14445 assert!(TextShapingCache::use_old_layout(&c, &c, &space, &space));
14446 }
14447
14448 #[test]
14449 fn inline_content_layout_eq_recurses_into_ruby() {
14450 let ruby = |base: &str| InlineContent::Ruby {
14451 base: vec![text_content(base, style())],
14452 text: vec![text_content("ふり", style())],
14453 style: style(),
14454 };
14455 assert!(TextShapingCache::inline_content_layout_eq(
14456 &ruby("漢"),
14457 &ruby("漢")
14458 ));
14459 assert!(!TextShapingCache::inline_content_layout_eq(
14460 &ruby("漢"),
14461 &ruby("字")
14462 ));
14463 }
14464
14465 #[test]
14466 fn calculate_id_is_deterministic_and_discriminating() {
14467 assert_eq!(calculate_id(&"abc"), calculate_id(&"abc"));
14468 assert_ne!(calculate_id(&"abc"), calculate_id(&"abd"));
14469 assert_eq!(calculate_id(&0_u64), calculate_id(&0_u64));
14470 assert_ne!(calculate_id(&0_u64), calculate_id(&u64::MAX));
14471 let e: Vec<u8> = Vec::new();
14473 assert_eq!(calculate_id(&e), calculate_id(&Vec::<u8>::new()));
14474 }
14475
14476 #[test]
14477 fn shaped_items_key_new_on_empty_visual_items_is_stable() {
14478 let a = ShapedItemsKey::new(7, &[]);
14479 let b = ShapedItemsKey::new(7, &[]);
14480 assert_eq!(a, b);
14481 assert_eq!(hash_of(&a), hash_of(&b));
14482 assert_ne!(a, ShapedItemsKey::new(8, &[]));
14484 }
14485
14486 #[test]
14487 fn shaped_items_key_new_hashes_the_text_styles() {
14488 let vi = |st: Arc<StyleProperties>| VisualItem {
14489 logical_source: LogicalItem::Text {
14490 source: ci(0, 0),
14491 text: "a".to_string(),
14492 style: st,
14493 marker_position_outside: None,
14494 source_node_id: None,
14495 },
14496 bidi_level: BidiLevel::new(0),
14497 script: Script::Latin,
14498 text: "a".to_string(),
14499 run_byte_offset: 0,
14500 };
14501 let base = ShapedItemsKey::new(1, &[vi(style())]);
14502 let same = ShapedItemsKey::new(1, &[vi(style())]);
14503 let other = ShapedItemsKey::new(1, &[vi(styled(|s| s.font_size_px = 32.0))]);
14504 assert_eq!(base, same);
14505 assert_ne!(base.style_hash, other.style_hash, "font size must change the key");
14506 }
14507
14508 #[test]
14513 fn overflow_info_default_has_no_overflow() {
14514 let o = OverflowInfo::default();
14515 assert!(!o.has_overflow());
14516 assert_eq!(o.unclipped_bounds, Rect::default());
14517
14518 let with = OverflowInfo {
14519 overflow_items: vec![cl("a", 8.0)],
14520 unclipped_bounds: Rect::default(),
14521 };
14522 assert!(with.has_overflow());
14523 }
14524
14525 fn layout_of(items: Vec<PositionedItem>) -> UnifiedLayout {
14526 UnifiedLayout {
14527 items,
14528 overflow: OverflowInfo::default(),
14529 }
14530 }
14531
14532 #[test]
14533 fn unified_layout_empty_is_inert_across_every_accessor() {
14534 let l = layout_of(Vec::new());
14535 assert!(l.is_empty());
14536 assert_eq!(l.bounds(), Rect::default());
14537 assert_eq!(l.first_baseline(), None);
14538 assert_eq!(l.last_baseline(), None);
14539 assert_eq!(l.get_first_cluster_cursor(), None);
14540 assert_eq!(l.get_last_cluster_cursor(), None);
14541 assert!(l.grapheme_stops().is_empty());
14542 assert_eq!(
14543 l.hittest_cursor(LogicalPosition { x: 0.0, y: 0.0 }),
14544 None,
14545 "hit-testing an empty layout must return None, not index [0]"
14546 );
14547 assert_eq!(
14548 l.hittest_cursor(LogicalPosition {
14549 x: f32::NAN,
14550 y: f32::NAN
14551 }),
14552 None
14553 );
14554 }
14555
14556 #[test]
14557 fn unified_layout_bounds_spans_all_items() {
14558 let l = layout_of(vec![
14559 pos(cl("a", 10.0), 0.0, 0.0, 0),
14560 pos(cl("b", 10.0), 90.0, 20.0, 1),
14561 ]);
14562 let b = l.bounds();
14563 assert_eq!(b.x, 0.0);
14564 assert_eq!(b.y, 0.0);
14565 assert_eq!(b.width, 100.0, "0 → 90+10");
14566 approx(b.height, 36.0); assert!(!l.is_empty());
14568 }
14569
14570 #[test]
14571 fn unified_layout_baselines_skip_breaks_and_tabs() {
14572 let l = layout_of(vec![
14573 pos(brk(), 0.0, 0.0, 0),
14574 pos(cl("a", 10.0), 0.0, 0.0, 0),
14575 pos(obj(10.0, 20.0, 5.0), 10.0, 0.0, 0),
14576 pos(tab(8.0, 16.0), 20.0, 0.0, 0),
14577 ]);
14578 approx(
14579 l.first_baseline().expect("the cluster, not the break"),
14580 12.8,
14581 );
14582 assert_eq!(l.last_baseline(), Some(5.0), "the object, not the tab");
14583 }
14584
14585 #[test]
14586 fn unified_layout_cluster_cursors_skip_non_clusters() {
14587 let l = layout_of(vec![
14588 pos(brk(), 0.0, 0.0, 0),
14589 pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0),
14590 pos(cl_at("b", 10.0, 0, 1), 10.0, 0.0, 0),
14591 pos(tab(8.0, 16.0), 20.0, 0.0, 0),
14592 ]);
14593 assert_eq!(
14594 l.get_first_cluster_cursor(),
14595 Some(TextCursor {
14596 cluster_id: gid(0, 0),
14597 affinity: CursorAffinity::Leading
14598 })
14599 );
14600 assert_eq!(
14601 l.get_last_cluster_cursor(),
14602 Some(TextCursor {
14603 cluster_id: gid(0, 1),
14604 affinity: CursorAffinity::Trailing
14605 })
14606 );
14607
14608 let no_clusters = layout_of(vec![pos(brk(), 0.0, 0.0, 0)]);
14610 assert_eq!(no_clusters.get_first_cluster_cursor(), None);
14611 assert_eq!(no_clusters.get_last_cluster_cursor(), None);
14612 }
14613
14614 #[test]
14615 fn unified_layout_grapheme_stops_sorts_dedups_and_folds_combining_marks() {
14616 let l = layout_of(vec![
14618 pos(cl_at("b", 10.0, 0, 1), 10.0, 0.0, 0),
14619 pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0),
14620 pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0), pos(cl_at("\u{0301}", 0.0, 0, 2), 20.0, 0.0, 0), ]);
14623 let stops = l.grapheme_stops();
14624 assert_eq!(
14625 stops,
14626 vec![gid(0, 0), gid(0, 1)],
14627 "sorted, de-duplicated, with the combining mark folded away"
14628 );
14629 }
14630
14631 #[test]
14632 fn unified_layout_cluster_is_grapheme_continuation() {
14633 assert!(UnifiedLayout::cluster_is_grapheme_continuation("\u{0301}"));
14634 assert!(UnifiedLayout::cluster_is_grapheme_continuation("\u{FE0F}"), "VS-16");
14635 assert!(!UnifiedLayout::cluster_is_grapheme_continuation("a"));
14636 assert!(!UnifiedLayout::cluster_is_grapheme_continuation("中"));
14637 assert!(
14638 !UnifiedLayout::cluster_is_grapheme_continuation(""),
14639 "an empty cluster must return false, not panic"
14640 );
14641 }
14642
14643 #[test]
14644 fn unified_layout_grapheme_caret_offset_maps_affinity_and_gaps() {
14645 let stops = [gid(0, 0), gid(0, 1), gid(0, 2)];
14646 assert_eq!(
14647 UnifiedLayout::grapheme_caret_offset(
14648 &stops,
14649 &TextCursor {
14650 cluster_id: gid(0, 0),
14651 affinity: CursorAffinity::Leading
14652 }
14653 ),
14654 Some(0)
14655 );
14656 assert_eq!(
14657 UnifiedLayout::grapheme_caret_offset(
14658 &stops,
14659 &TextCursor {
14660 cluster_id: gid(0, 2),
14661 affinity: CursorAffinity::Trailing
14662 }
14663 ),
14664 Some(3),
14665 "the document end is len, i.e. one past the last stop"
14666 );
14667 assert_eq!(
14669 UnifiedLayout::grapheme_caret_offset(
14670 &stops,
14671 &TextCursor {
14672 cluster_id: gid(0, 99),
14673 affinity: CursorAffinity::Leading
14674 }
14675 ),
14676 Some(2)
14677 );
14678 assert_eq!(
14680 UnifiedLayout::grapheme_caret_offset(
14681 &[gid(5, 5)],
14682 &TextCursor {
14683 cluster_id: gid(0, 0),
14684 affinity: CursorAffinity::Leading
14685 }
14686 ),
14687 None
14688 );
14689 assert_eq!(
14691 UnifiedLayout::grapheme_caret_offset(
14692 &[],
14693 &TextCursor {
14694 cluster_id: gid(0, 0),
14695 affinity: CursorAffinity::Leading
14696 }
14697 ),
14698 None
14699 );
14700 }
14701
14702 #[test]
14703 fn unified_layout_cursor_from_grapheme_offset_clamps_past_the_end() {
14704 let stops = [gid(0, 0), gid(0, 1)];
14705 assert_eq!(
14706 UnifiedLayout::cursor_from_grapheme_offset(&stops, 0),
14707 TextCursor {
14708 cluster_id: gid(0, 0),
14709 affinity: CursorAffinity::Leading
14710 }
14711 );
14712 assert_eq!(
14713 UnifiedLayout::cursor_from_grapheme_offset(&stops, 1),
14714 TextCursor {
14715 cluster_id: gid(0, 1),
14716 affinity: CursorAffinity::Leading
14717 }
14718 );
14719 let end = TextCursor {
14721 cluster_id: gid(0, 1),
14722 affinity: CursorAffinity::Trailing,
14723 };
14724 assert_eq!(UnifiedLayout::cursor_from_grapheme_offset(&stops, 2), end);
14725 assert_eq!(
14726 UnifiedLayout::cursor_from_grapheme_offset(&stops, usize::MAX),
14727 end,
14728 "usize::MAX must clamp, not overflow"
14729 );
14730 }
14731
14732 #[test]
14733 #[should_panic]
14734 fn unified_layout_cursor_from_grapheme_offset_panics_on_an_empty_stop_list() {
14735 let _ = UnifiedLayout::cursor_from_grapheme_offset(&[], 0);
14740 }
14741
14742 #[test]
14743 fn unified_layout_cursor_motion_on_an_empty_layout_returns_the_cursor_unchanged() {
14744 let l = layout_of(Vec::new());
14745 let c = TextCursor {
14746 cluster_id: gid(0, 0),
14747 affinity: CursorAffinity::Leading,
14748 };
14749 let mut dbg = None;
14750 assert_eq!(l.move_cursor_left(c, &mut dbg), c);
14751 assert_eq!(l.move_cursor_right(c, &mut dbg), c);
14752 assert_eq!(l.move_cursor_to_line_start(c, &mut dbg), c);
14753 assert_eq!(l.move_cursor_to_line_end(c, &mut dbg), c);
14754 assert_eq!(l.move_cursor_to_prev_word(c, &mut dbg), c);
14755 assert_eq!(l.move_cursor_to_next_word(c, &mut dbg), c);
14756 let mut goal = None;
14757 assert_eq!(l.move_cursor_up(c, &mut goal, &mut dbg), c);
14758 assert_eq!(l.move_cursor_down(c, &mut goal, &mut dbg), c);
14759 }
14760
14761 #[test]
14762 fn unified_layout_move_cursor_left_right_walk_one_grapheme_at_a_time() {
14763 let l = layout_of(vec![
14764 pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0),
14765 pos(cl_at("b", 10.0, 0, 1), 10.0, 0.0, 0),
14766 pos(cl_at("c", 10.0, 0, 2), 20.0, 0.0, 0),
14767 ]);
14768 let mut dbg = None;
14769 let start = TextCursor {
14770 cluster_id: gid(0, 0),
14771 affinity: CursorAffinity::Leading,
14772 };
14773
14774 assert_eq!(l.move_cursor_left(start, &mut dbg), start);
14776
14777 let c1 = l.move_cursor_right(start, &mut dbg);
14779 assert_eq!(c1.cluster_id, gid(0, 1));
14780 let c2 = l.move_cursor_right(c1, &mut dbg);
14781 assert_eq!(c2.cluster_id, gid(0, 2));
14782 let end = l.move_cursor_right(c2, &mut dbg);
14783 assert_eq!(end.cluster_id, gid(0, 2));
14784 assert_eq!(end.affinity, CursorAffinity::Trailing, "document end");
14785 assert_eq!(l.move_cursor_right(end, &mut dbg), end);
14787
14788 assert_eq!(l.move_cursor_left(end, &mut dbg).cluster_id, gid(0, 2));
14790 }
14791
14792 #[test]
14793 fn unified_layout_hittest_cursor_picks_the_nearest_cluster_and_its_half() {
14794 let l = layout_of(vec![
14795 pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0),
14796 pos(cl_at("b", 10.0, 0, 1), 10.0, 0.0, 0),
14797 ]);
14798 let hit = |x: f32| l.hittest_cursor(LogicalPosition { x, y: 5.0 }).unwrap();
14799 assert_eq!(hit(1.0).cluster_id, gid(0, 0));
14800 assert_eq!(hit(1.0).affinity, CursorAffinity::Leading);
14801 assert_eq!(hit(9.0).affinity, CursorAffinity::Trailing, "right half of 'a'");
14802 assert_eq!(hit(11.0).cluster_id, gid(0, 1));
14803 assert_eq!(hit(-1000.0).cluster_id, gid(0, 0));
14805 assert_eq!(hit(1000.0).cluster_id, gid(0, 1));
14806 }
14807
14808 #[test]
14809 fn unified_layout_get_selection_rects_on_an_unknown_range_is_empty() {
14810 let l = layout_of(vec![pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0)]);
14811 let unknown = SelectionRange {
14812 start: TextCursor {
14813 cluster_id: gid(9, 9),
14814 affinity: CursorAffinity::Leading,
14815 },
14816 end: TextCursor {
14817 cluster_id: gid(9, 9),
14818 affinity: CursorAffinity::Trailing,
14819 },
14820 };
14821 assert!(l.get_selection_rects(&unknown).is_empty());
14822
14823 let collapsed = SelectionRange {
14825 start: TextCursor {
14826 cluster_id: gid(0, 0),
14827 affinity: CursorAffinity::Leading,
14828 },
14829 end: TextCursor {
14830 cluster_id: gid(0, 0),
14831 affinity: CursorAffinity::Leading,
14832 },
14833 };
14834 let _ = l.get_selection_rects(&collapsed);
14835 }
14836
14837 #[test]
14838 fn unified_layout_get_cursor_rect_for_known_and_unknown_cursors() {
14839 let l = layout_of(vec![pos(cl_at("a", 10.0, 0, 0), 5.0, 7.0, 0)]);
14840 let leading = l.get_cursor_rect(&TextCursor {
14841 cluster_id: gid(0, 0),
14842 affinity: CursorAffinity::Leading,
14843 });
14844 let r = leading.expect("the leading edge of a placed cluster must have a rect");
14845 assert_eq!(r.origin.x, 5.0);
14846 assert_eq!(r.origin.y, 7.0);
14847 assert_eq!(r.size.width, 1.0, "the caret is a 1px sliver");
14848
14849 assert_eq!(
14851 l.get_cursor_rect(&TextCursor {
14852 cluster_id: gid(9, 0),
14853 affinity: CursorAffinity::Leading
14854 }),
14855 None
14856 );
14857 assert_eq!(
14859 layout_of(Vec::new()).get_cursor_rect(&TextCursor {
14860 cluster_id: gid(0, 0),
14861 affinity: CursorAffinity::Leading
14862 }),
14863 None
14864 );
14865 }
14866
14867 #[test]
14872 fn break_cursor_new_on_an_empty_slice_is_both_at_start_and_done() {
14873 let items: Vec<ShapedItem> = Vec::new();
14874 let mut c = BreakCursor::new(&items);
14875 assert!(c.is_at_start());
14876 assert!(c.is_done());
14877 assert_eq!(c.word_break, WordBreak::Normal);
14878 assert_eq!(c.hyphens, Hyphens::default());
14879 assert_eq!(c.line_break, LineBreakStrictness::default());
14880 assert!(c.peek_next_unit().is_empty());
14881 assert!(c.peek_next_single_item().is_empty());
14882 assert!(c.drain_remaining().is_empty());
14883 }
14884
14885 #[test]
14886 fn break_cursor_with_word_break_stores_the_mode() {
14887 let items = vec![cl("a", 8.0)];
14888 let c = BreakCursor::with_word_break(&items, WordBreak::BreakAll);
14889 assert_eq!(c.word_break, WordBreak::BreakAll);
14890 assert!(c.is_at_start());
14891 assert!(!c.is_done());
14892 }
14893
14894 #[test]
14895 fn break_cursor_consume_zero_is_a_no_op() {
14896 let items = vec![cl("a", 8.0), cl("b", 8.0)];
14897 let mut c = BreakCursor::new(&items);
14898 c.consume(0);
14899 assert!(c.is_at_start());
14900 assert_eq!(c.next_item_index, 0);
14901 }
14902
14903 #[test]
14904 fn break_cursor_consume_advances_and_ends_the_stream() {
14905 let items = vec![cl("a", 8.0), cl("b", 8.0)];
14906 let mut c = BreakCursor::new(&items);
14907 c.consume(1);
14908 assert!(!c.is_at_start());
14909 assert!(!c.is_done());
14910 assert_eq!(c.peek_next_single_item().len(), 1);
14911 c.consume(1);
14912 assert!(c.is_done());
14913 assert!(c.peek_next_single_item().is_empty());
14914 }
14915
14916 #[test]
14917 fn break_cursor_over_consuming_past_the_end_still_reports_done() {
14918 let items = vec![cl("a", 8.0)];
14919 let mut c = BreakCursor::new(&items);
14920 c.consume(usize::MAX);
14921 assert!(c.is_done(), "an over-consume must not wrap around to not-done");
14922 assert!(
14923 c.drain_remaining().is_empty(),
14924 "drain_remaining is bounds-guarded"
14925 );
14926 }
14927
14928 #[test]
14929 #[should_panic]
14930 fn break_cursor_peek_next_unit_after_over_consuming_slices_out_of_bounds() {
14931 let items = vec![cl("a", 8.0)];
14936 let mut c = BreakCursor::new(&items);
14937 c.consume(5);
14938 let _ = c.peek_next_unit();
14939 }
14940
14941 #[test]
14942 fn break_cursor_drains_the_remainder_before_the_main_list() {
14943 let items = vec![cl("a", 8.0), cl("b", 8.0)];
14944 let mut c = BreakCursor::new(&items);
14945 c.partial_remainder = vec![cl("R", 8.0)];
14946 assert!(!c.is_at_start(), "a pending remainder means we are mid-stream");
14947 assert!(!c.is_done());
14948
14949 assert_eq!(
14950 c.peek_next_single_item()[0].as_cluster().unwrap().text,
14951 "R",
14952 "the remainder is served first"
14953 );
14954
14955 let drained = c.drain_remaining();
14956 assert_eq!(drained.len(), 3, "remainder + both queued items");
14957 assert_eq!(drained[0].as_cluster().unwrap().text, "R");
14958 assert!(c.is_done());
14959 }
14960
14961 #[test]
14962 fn break_cursor_is_done_is_false_while_a_remainder_is_pending() {
14963 let items: Vec<ShapedItem> = Vec::new();
14964 let mut c = BreakCursor::new(&items);
14965 c.partial_remainder = vec![cl("x", 8.0)];
14966 assert!(!c.is_done(), "the main list is exhausted but the remainder is not");
14967 c.consume(1);
14968 assert!(c.is_done());
14969 }
14970
14971 #[test]
14972 fn break_cursor_consume_spanning_remainder_and_main_list() {
14973 let items = vec![cl("a", 8.0), cl("b", 8.0), cl("c", 8.0)];
14974 let mut c = BreakCursor::new(&items);
14975 c.partial_remainder = vec![cl("R1", 8.0), cl("R2", 8.0)];
14976 c.consume(3);
14978 assert!(c.partial_remainder.is_empty());
14979 assert_eq!(c.next_item_index, 1);
14980 assert_eq!(c.peek_next_single_item()[0].as_cluster().unwrap().text, "b");
14981 }
14982
14983 #[test]
14984 fn break_cursor_peek_next_unit_returns_a_whole_word_then_the_space() {
14985 let items = vec![
14986 cl("h", 8.0),
14987 cl("i", 8.0),
14988 cl(" ", 4.0),
14989 cl("y", 8.0),
14990 cl("o", 8.0),
14991 ];
14992 let mut c = BreakCursor::new(&items);
14993 let word = c.peek_next_unit();
14994 assert_eq!(word.len(), 2, "the word stops at the space");
14995 assert_eq!(word[0].as_cluster().unwrap().text, "h");
14996
14997 c.consume(word.len());
14998 let space = c.peek_next_unit();
14999 assert_eq!(space.len(), 1, "a leading break opportunity is a unit on its own");
15000 assert_eq!(space[0].as_cluster().unwrap().text, " ");
15001
15002 c.consume(1);
15003 assert_eq!(c.peek_next_unit().len(), 2, "the trailing word");
15004 }
15005
15006 #[test]
15007 fn break_cursor_peek_next_unit_honours_break_all_and_keep_all() {
15008 let items = vec![cl("中", 16.0), cl("文", 16.0), cl("字", 16.0)];
15009
15010 let normal = BreakCursor::new(&items);
15011 assert_eq!(
15012 normal.peek_next_unit().len(),
15013 1,
15014 "word-break:normal — each ideograph is its own unit"
15015 );
15016
15017 let all = BreakCursor::with_word_break(&items, WordBreak::BreakAll);
15018 assert_eq!(all.peek_next_unit().len(), 1, "break-all — one cluster per unit");
15019
15020 let keep = BreakCursor::with_word_break(&items, WordBreak::KeepAll);
15021 assert_eq!(
15022 keep.peek_next_unit().len(),
15023 3,
15024 "keep-all — the whole CJK run is unbreakable"
15025 );
15026
15027 let latin = vec![cl("a", 8.0), cl("b", 8.0)];
15028 let latin_all = BreakCursor::with_word_break(&latin, WordBreak::BreakAll);
15029 assert_eq!(latin_all.peek_next_unit().len(), 1, "break-all splits Latin too");
15030 }
15031
15032 #[test]
15033 fn break_cursor_peek_next_unit_glues_across_a_word_joiner() {
15034 let plain = vec![cl("a", 8.0), cl(" ", 4.0), cl("b", 8.0)];
15036 let control = BreakCursor::new(&plain);
15037 assert_eq!(
15038 control.peek_next_unit().len(),
15039 1,
15040 "the unit normally stops before the space"
15041 );
15042
15043 let glued = vec![
15046 cl("a", 8.0),
15047 cl("\u{2060}", 0.0),
15048 cl(" ", 4.0),
15049 cl("b", 8.0),
15050 ];
15051 let c = BreakCursor::new(&glued);
15052 let unit = c.peek_next_unit();
15053 assert!(
15054 unit.len() > 1,
15055 "a word joiner must suppress the following break, got {} item(s)",
15056 unit.len()
15057 );
15058 }
15059
15060 #[test]
15061 fn break_cursor_peek_next_single_item_prefers_the_remainder() {
15062 let items = vec![cl("a", 8.0)];
15063 let mut c = BreakCursor::new(&items);
15064 assert_eq!(c.peek_next_single_item()[0].as_cluster().unwrap().text, "a");
15065 c.partial_remainder = vec![cl("R", 8.0)];
15066 assert_eq!(c.peek_next_single_item()[0].as_cluster().unwrap().text, "R");
15067 assert_eq!(
15068 c.peek_next_single_item().len(),
15069 1,
15070 "peek must never return more than one item"
15071 );
15072 }
15073
15074 fn tf(hash: u64) -> TestFont {
15079 TestFont { hash }
15080 }
15081
15082 #[test]
15083 fn loaded_fonts_new_is_empty_and_misses_every_lookup() {
15084 let lf: LoadedFonts<TestFont> = LoadedFonts::new();
15085 assert!(lf.is_empty());
15086 assert_eq!(lf.len(), 0);
15087 assert_eq!(lf.iter().count(), 0);
15088 assert!(lf.get(&FontId(0)).is_none());
15089 assert!(!lf.contains_key(&FontId(u128::MAX)));
15090 for h in [0_u64, 1, u64::MAX] {
15092 assert!(lf.get_by_hash(h).is_none());
15093 assert!(lf.get_font_id_by_hash(h).is_none());
15094 assert!(!lf.contains_hash(h));
15095 }
15096 let d: LoadedFonts<TestFont> = LoadedFonts::default();
15098 assert!(d.is_empty());
15099 }
15100
15101 #[test]
15102 fn loaded_fonts_insert_indexes_by_id_and_by_hash() {
15103 let mut lf: LoadedFonts<TestFont> = LoadedFonts::new();
15104 lf.insert(FontId(1), tf(0xDEAD));
15105 assert_eq!(lf.len(), 1);
15106 assert!(!lf.is_empty());
15107 assert!(lf.contains_key(&FontId(1)));
15108 assert!(lf.contains_hash(0xDEAD));
15109 assert_eq!(lf.get(&FontId(1)).map(TestFont::get_hash), Some(0xDEAD));
15110 assert_eq!(lf.get_by_hash(0xDEAD).map(TestFont::get_hash), Some(0xDEAD));
15111 assert_eq!(lf.get_font_id_by_hash(0xDEAD), Some(&FontId(1)));
15112 assert!(lf.get_by_hash(0).is_none());
15113 }
15114
15115 #[test]
15116 fn loaded_fonts_zero_hash_is_a_valid_key_not_a_sentinel() {
15117 let mut lf: LoadedFonts<TestFont> = LoadedFonts::new();
15118 lf.insert(FontId(1), tf(0));
15119 assert!(lf.contains_hash(0), "hash 0 must be storable and findable");
15120 assert_eq!(lf.get_by_hash(0).map(TestFont::get_hash), Some(0));
15121 }
15122
15123 #[test]
15124 fn loaded_fonts_two_ids_sharing_a_hash_keep_only_the_last_reverse_mapping() {
15125 let mut lf: LoadedFonts<TestFont> = LoadedFonts::new();
15126 lf.insert(FontId(1), tf(7));
15127 lf.insert(FontId(2), tf(7));
15128 assert_eq!(lf.len(), 2, "both fonts are stored by id");
15129 assert_eq!(
15130 lf.get_font_id_by_hash(7),
15131 Some(&FontId(2)),
15132 "the reverse index keeps only the LAST id for a colliding hash"
15133 );
15134 }
15135
15136 #[test]
15137 fn loaded_fonts_replacing_a_font_id_leaves_a_stale_hash_mapping() {
15138 let mut lf: LoadedFonts<TestFont> = LoadedFonts::new();
15143 lf.insert(FontId(1), tf(100));
15144 lf.insert(FontId(1), tf(200)); assert_eq!(lf.len(), 1, "the font map correctly holds one entry");
15146
15147 assert!(
15148 lf.contains_hash(100),
15149 "the old hash is still in the reverse index"
15150 );
15151 let stale = lf.get_by_hash(100).expect("stale mapping resolves");
15152 assert_eq!(
15153 stale.get_hash(),
15154 200,
15155 "looking up the OLD hash hands back the NEW font"
15156 );
15157 }
15158
15159 #[test]
15160 fn loaded_fonts_from_iterator_matches_repeated_inserts() {
15161 let lf: LoadedFonts<TestFont> =
15162 vec![(FontId(1), tf(10)), (FontId(2), tf(20))].into_iter().collect();
15163 assert_eq!(lf.len(), 2);
15164 assert!(lf.contains_hash(10) && lf.contains_hash(20));
15165 assert_eq!(lf.iter().count(), 2);
15166 }
15167
15168 fn manager() -> FontManager<TestFont> {
15173 FontManager::new(FcFontCache::default()).expect("FontManager::new must not fail")
15174 }
15175
15176 #[test]
15177 fn font_manager_constructors_start_empty() {
15178 for m in [
15179 manager(),
15180 FontManager::from_shared(FcFontCache::default()).unwrap(),
15181 FontManager::from_arc_shared(
15182 FcFontCache::default(),
15183 Arc::new(Mutex::new(HashMap::new())),
15184 )
15185 .unwrap(),
15186 ] {
15187 assert!(m.get_font_chain_cache().is_empty());
15188 assert!(m.get_loaded_fonts().is_empty());
15189 assert!(m.get_loaded_font_ids().is_empty());
15190 assert!(m.registry.is_none());
15191 assert_eq!(m.last_resolved_font_stacks_sig, None);
15192 assert!(m.get_font_by_hash(0).is_none());
15193 assert!(m.get_embedded_font_by_hash(u64::MAX).is_none());
15194 }
15195 }
15196
15197 #[test]
15198 fn font_manager_from_arc_shared_sees_writes_through_the_shared_pool() {
15199 let pool: Arc<Mutex<HashMap<FontId, TestFont>>> = Arc::new(Mutex::new(HashMap::new()));
15200 let a = FontManager::from_arc_shared(FcFontCache::default(), pool.clone()).unwrap();
15201 let b = FontManager::from_arc_shared(FcFontCache::default(), pool).unwrap();
15202
15203 assert!(a.insert_font(FontId(1), tf(5)).is_none(), "no previous font");
15204 assert_eq!(
15205 b.get_loaded_fonts().len(),
15206 1,
15207 "the second manager must observe the first's insert"
15208 );
15209 assert_eq!(b.get_font_by_hash(5).map(|f| f.get_hash()), Some(5));
15210
15211 assert!(Arc::ptr_eq(&a.shared_parsed_fonts(), &b.shared_parsed_fonts()));
15213 }
15214
15215 #[test]
15216 fn font_manager_insert_font_returns_the_replaced_font() {
15217 let m = manager();
15218 assert!(m.insert_font(FontId(1), tf(1)).is_none());
15219 let old = m.insert_font(FontId(1), tf(2)).expect("must return the old font");
15220 assert_eq!(old.get_hash(), 1);
15221 assert_eq!(m.get_loaded_fonts().len(), 1);
15222 }
15223
15224 #[test]
15225 fn font_manager_insert_fonts_and_remove_font() {
15226 let m = manager();
15227 m.insert_fonts(vec![(FontId(1), tf(1)), (FontId(2), tf(2))]);
15228 assert_eq!(m.get_loaded_font_ids().len(), 2);
15229
15230 assert_eq!(m.remove_font(&FontId(1)).map(|f| f.get_hash()), Some(1));
15231 assert!(m.remove_font(&FontId(1)).is_none(), "double-remove is a no-op");
15232 assert!(
15233 m.remove_font(&FontId(u128::MAX)).is_none(),
15234 "removing an unknown id must not panic"
15235 );
15236 assert_eq!(m.get_loaded_fonts().len(), 1);
15237
15238 m.insert_fonts(Vec::new());
15240 assert_eq!(m.get_loaded_fonts().len(), 1);
15241 }
15242
15243 #[test]
15244 fn font_manager_get_font_by_hash_scans_linearly_and_misses_cleanly() {
15245 let m = manager();
15246 m.insert_fonts(vec![(FontId(1), tf(11)), (FontId(2), tf(22))]);
15247 assert_eq!(m.get_font_by_hash(22).map(|f| f.get_hash()), Some(22));
15248 assert!(m.get_font_by_hash(33).is_none());
15249 assert!(m.get_font_by_hash(u64::MAX).is_none());
15250 assert!(m.get_font_by_hash(0).is_none());
15251 }
15252
15253 #[test]
15254 fn font_manager_chain_cache_set_merge_and_signature() {
15255 let mut m = manager();
15256 assert!(m.get_font_chain_cache().is_empty());
15257
15258 m.set_font_chain_cache_with_sig(HashMap::new(), Some(42));
15260 assert_eq!(m.last_resolved_font_stacks_sig, Some(42));
15261
15262 m.set_font_chain_cache(HashMap::new());
15264 assert_eq!(
15265 m.last_resolved_font_stacks_sig, None,
15266 "a signature-less set must invalidate the recorded signature"
15267 );
15268
15269 m.merge_font_chain_cache(HashMap::new());
15271 assert!(m.get_font_chain_cache().is_empty());
15272 }
15273
15274 #[test]
15275 fn font_manager_garbage_collect_evicts_everything_not_in_the_keep_set() {
15276 let mut m = manager();
15277 m.insert_fonts(vec![
15278 (FontId(1), tf(1)),
15279 (FontId(2), tf(2)),
15280 (FontId(3), tf(3)),
15281 ]);
15282
15283 let mut keep = HashSet::new();
15284 keep.insert(FontId(2));
15285 let evicted = m.garbage_collect_fonts(&keep, &HashSet::new());
15286 assert_eq!(evicted, 2);
15287 assert_eq!(m.get_loaded_font_ids(), keep);
15288
15289 assert_eq!(m.garbage_collect_fonts(&keep, &HashSet::new()), 0);
15291
15292 assert_eq!(m.garbage_collect_fonts(&HashSet::new(), &HashSet::new()), 1);
15294 assert!(m.get_loaded_fonts().is_empty());
15295 assert_eq!(m.garbage_collect_fonts(&HashSet::new(), &HashSet::new()), 0);
15297 }
15298
15299 #[test]
15300 fn font_manager_load_missing_for_chains_with_no_chains_loads_nothing() {
15301 use crate::solver3::getters::ResolvedFontChains;
15302 let m = manager();
15303 let empty = ResolvedFontChains {
15304 chains: HashMap::new(),
15305 ..Default::default()
15306 };
15307 let failed = m.load_missing_for_chains(
15308 &empty,
15309 |_bytes, _idx| -> Result<TestFont, LayoutError> {
15310 panic!("the loader must never be invoked when there is nothing to load")
15311 },
15312 );
15313 assert!(failed.is_empty());
15314 assert!(m.get_loaded_fonts().is_empty());
15315 }
15316
15317 #[test]
15318 fn font_context_from_fc_cache_starts_empty_and_converts_to_a_manager() {
15319 let ctx = FontContext::from_fc_cache(FcFontCache::default());
15320 assert!(ctx.font_chain_cache.is_empty());
15321 assert!(ctx.embedded_fonts.is_empty());
15322 assert!(ctx.font_hash_to_families.is_empty());
15323 assert!(ctx.registry.is_none());
15324 assert!(ctx.parsed_fonts.lock().unwrap().is_empty());
15325
15326 ctx.load_fonts_for_chains();
15328 assert!(ctx.parsed_fonts.lock().unwrap().is_empty());
15329
15330 let mgr = ctx.to_font_manager();
15331 assert!(mgr.get_font_chain_cache().is_empty());
15332 assert!(mgr.registry.is_none());
15333 assert_eq!(mgr.last_resolved_font_stacks_sig, None);
15334 assert!(
15335 Arc::ptr_eq(&mgr.parsed_fonts, &ctx.parsed_fonts),
15336 "the manager must share (not copy) the parsed-font pool"
15337 );
15338 }
15339
15340 #[test]
15345 fn create_logical_items_on_empty_and_whitespace_only_content() {
15346 let mut dbg = None;
15347 assert!(create_logical_items(&[], &[], &mut dbg).is_empty());
15348
15349 let empty_run = [text_content("", style())];
15351 assert!(create_logical_items(&empty_run, &[], &mut dbg).is_empty());
15352
15353 let ws = [text_content(" \t\n", style())];
15355 assert!(!create_logical_items(&ws, &[], &mut dbg).is_empty());
15356 }
15357
15358 #[test]
15359 fn create_logical_items_handles_multibyte_and_astral_text_without_panicking() {
15360 let mut dbg = None;
15361 for s in ["\u{1F600}", "é\u{0301}", "中文", "a\u{200B}b", "\u{FEFF}"] {
15362 let content = [text_content(s, style())];
15363 let items = create_logical_items(&content, &[], &mut dbg);
15364 assert!(!items.is_empty(), "{s:?} produced no logical items");
15365 }
15366 }
15367
15368 #[test]
15369 fn create_logical_items_on_a_long_run_does_not_hang() {
15370 let mut dbg = None;
15371 let long = "a".repeat(100_000);
15372 let content = [text_content(&long, style())];
15373 let items = create_logical_items(&content, &[], &mut dbg);
15374 assert!(!items.is_empty());
15375 }
15376
15377 #[test]
15378 fn create_logical_items_debug_messages_are_recorded_when_requested() {
15379 let mut dbg = Some(Vec::new());
15380 let content = [text_content("hi", style())];
15381 let _ = create_logical_items(&content, &[], &mut dbg);
15382 assert!(
15383 !dbg.expect("Some(..) in → Some(..) out").is_empty(),
15384 "the debug sink must be populated when it is Some"
15385 );
15386 }
15387
15388 #[test]
15389 fn get_base_direction_from_logical_defaults_to_ltr_on_empty_input() {
15390 assert_eq!(get_base_direction_from_logical(&[]), BidiDirection::Ltr);
15391
15392 let mut dbg = None;
15393 let ltr = create_logical_items(&[text_content("hello", style())], &[], &mut dbg);
15394 assert_eq!(get_base_direction_from_logical(<r), BidiDirection::Ltr);
15395
15396 let rtl = create_logical_items(&[text_content("\u{05D0}\u{05D1}", style())], &[], &mut dbg);
15398 assert_eq!(get_base_direction_from_logical(&rtl), BidiDirection::Rtl);
15399 }
15400
15401 #[test]
15402 fn reorder_logical_items_on_empty_input_is_ok_and_empty() {
15403 let mut dbg = None;
15404 let out = reorder_logical_items(&[], BidiDirection::Ltr, UnicodeBidi::Normal, &mut dbg)
15405 .expect("reordering nothing must succeed");
15406 assert!(out.is_empty());
15407 }
15408
15409 #[test]
15410 fn reorder_logical_items_preserves_content_for_pure_ltr_text() {
15411 let mut dbg = None;
15412 let logical = create_logical_items(&[text_content("abc", style())], &[], &mut dbg);
15413 let visual = reorder_logical_items(
15414 &logical,
15415 BidiDirection::Ltr,
15416 UnicodeBidi::Normal,
15417 &mut dbg,
15418 )
15419 .expect("LTR reordering must succeed");
15420 let joined: String = visual.iter().map(|v| v.text.as_str()).collect();
15421 assert_eq!(joined, "abc", "pure LTR text must survive bidi unchanged");
15422 assert!(visual.iter().all(|v| !v.bidi_level.is_rtl()));
15423 }
15424
15425 #[cfg(not(feature = "text_layout_hyphenation"))]
15430 #[test]
15431 fn stub_hyphenate_never_reports_a_break() {
15432 let s = Standard;
15433 assert!(s.hyphenate("").breaks.is_empty());
15434 assert!(s.hyphenate("hyphenation").breaks.is_empty());
15435 assert!(s.hyphenate(&"a".repeat(10_000)).breaks.is_empty());
15436 assert!(s.hyphenate("\u{1F600}\u{0301}").breaks.is_empty());
15437 }
15438
15439 #[cfg(not(feature = "text_layout_hyphenation"))]
15440 #[test]
15441 fn stub_get_hyphenator_always_errors() {
15442 assert!(matches!(
15443 get_hyphenator(Language::EnglishUS),
15444 Err(LayoutError::HyphenationError(_))
15445 ));
15446 }
15447
15448 #[cfg(feature = "text_layout_hyphenation")]
15449 #[test]
15450 fn get_hyphenator_loads_an_embedded_language_and_hyphenates() {
15451 let h =
15452 get_hyphenator(HyphenationLanguage::EnglishUS).expect("en-US dictionaries are embedded");
15453
15454 let empty = h.hyphenate("");
15456 assert!(empty.breaks.is_empty(), "an empty word must not panic");
15457 let one = h.hyphenate("a");
15458 assert!(one.breaks.is_empty());
15459
15460 let word = "hyphenation";
15462 let opps = h.hyphenate(word);
15463 for &b in &opps.breaks {
15464 assert!(b > 0 && b < word.len(), "break {b} is outside {word:?}");
15465 assert!(word.is_char_boundary(b), "break {b} splits a char");
15466 }
15467
15468 let weird = h.hyphenate("\u{1F600}\u{0301}");
15470 assert!(weird.breaks.len() < 8, "no runaway break list");
15471 }
15472}