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)]
776pub struct MemoryFace {
777 pub font_match: rust_fontconfig::FontMatch,
779 pub weight: FcWeight,
782 pub italic: bool,
784 pub oblique: bool,
786 pub stretch: FcStretch,
788 pub weight_axis: Option<(f32, f32)>,
791}
792
793#[derive(Debug, Clone, Copy)]
796struct FaceStyle {
797 weight: FcWeight,
798 italic: bool,
799 oblique: bool,
800 stretch: FcStretch,
801 weight_axis: Option<(f32, f32)>,
802}
803
804impl Default for FaceStyle {
805 fn default() -> Self {
806 Self {
807 weight: FcWeight::Normal,
808 italic: false,
809 oblique: false,
810 stretch: FcStretch::Normal,
811 weight_axis: None,
812 }
813 }
814}
815
816fn parse_face_style(bytes: &[u8], family: &str) -> FaceStyle {
821 let Some(faces) = rust_fontconfig::FcParseFontBytes(bytes, family) else {
822 return FaceStyle::default();
823 };
824 let Some((pat, _)) = faces.into_iter().next() else {
825 return FaceStyle::default();
826 };
827 FaceStyle {
828 weight: pat.weight,
829 italic: pat.italic == PatternMatch::True,
830 oblique: pat.oblique == PatternMatch::True,
831 stretch: pat.stretch,
832 weight_axis: None,
833 }
834}
835
836#[derive(Debug)]
837pub struct FontManager<T> {
838 pub fc_cache: FcFontCache,
843 pub parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
847 pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
850 pub embedded_fonts: Mutex<HashMap<u64, azul_css::props::basic::FontRef>>,
853 pub font_hash_to_families: HashMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
857 pub registry: Option<Arc<rust_fontconfig::registry::FcFontRegistry>>,
864 pub last_resolved_font_stacks_sig: Option<u64>,
871 pub memory_families: HashMap<String, Vec<MemoryFace>>,
887 vf_bake_cache: HashMap<u64, Vec<(FontId, FaceStyle)>>,
892}
893
894impl<T: ParsedFontTrait> FontManager<T> {
895 pub fn new(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
899 let mut fm = Self {
900 fc_cache,
901 parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
902 font_chain_cache: HashMap::new(),
903 embedded_fonts: Mutex::new(HashMap::new()),
904 font_hash_to_families: HashMap::new(),
905 registry: None,
906 last_resolved_font_stacks_sig: None,
907 memory_families: HashMap::new(),
908 vf_bake_cache: HashMap::new(),
909 };
910 fm.register_builtin_mock_fonts();
911 Ok(fm)
912 }
913
914 pub fn register_named_font(
932 &mut self,
933 family: &str,
934 bytes: &[u8],
935 coverage: Vec<UnicodeRange>,
936 ) -> FontId {
937 let norm = rust_fontconfig::utils::normalize_family_name(family);
938
939 if let Some((min, def, max)) = crate::font::parsed::read_wght_axis(bytes, 0) {
946 if let Some(id) =
947 self.register_variable_instances(&norm, family, bytes, &coverage, min, def, max)
948 {
949 return id;
950 }
951 }
952
953 let style = parse_face_style(bytes, family);
959
960 let mut existing: Vec<(FontId, Vec<UnicodeRange>)> = Vec::new();
967 self.fc_cache.for_each_pattern(|pattern, id| {
968 let fam_hit = pattern
969 .family
970 .as_deref()
971 .is_some_and(|f| rust_fontconfig::utils::normalize_family_name(f) == norm);
972 let style_hit = pattern.weight == style.weight
973 && (pattern.italic == PatternMatch::True) == style.italic
974 && (pattern.oblique == PatternMatch::True) == style.oblique;
975 if fam_hit && style_hit {
976 existing.push((*id, pattern.unicode_ranges.clone()));
977 }
978 });
979 let id = if let Some((id, ranges)) = existing
980 .into_iter()
981 .find(|(id, _)| self.fc_cache.is_memory_font(id))
982 {
983 self.index_memory_face(&norm, id, ranges, &style);
984 id
985 } else {
986 let pattern = rust_fontconfig::FcPattern {
987 name: Some(family.to_string()),
988 family: Some(family.to_string()),
989 italic: if style.italic { PatternMatch::True } else { PatternMatch::False },
990 oblique: if style.oblique { PatternMatch::True } else { PatternMatch::False },
991 bold: if style.weight >= FcWeight::Bold { PatternMatch::True } else { PatternMatch::False },
992 weight: style.weight,
993 stretch: style.stretch,
994 unicode_ranges: coverage.clone(),
995 ..Default::default()
996 };
997 let id = FontId::new();
998 self.fc_cache.with_memory_font_with_id(
999 id,
1000 pattern,
1001 rust_fontconfig::FcFont {
1002 bytes: bytes.to_vec(),
1003 font_index: 0,
1004 id: family.to_string(),
1005 },
1006 );
1007 self.index_memory_face(&norm, id, coverage, &style);
1008 id
1009 };
1010 id
1011 }
1012
1013 fn index_memory_face(
1016 &mut self,
1017 norm: &str,
1018 id: FontId,
1019 unicode_ranges: Vec<UnicodeRange>,
1020 style: &FaceStyle,
1021 ) {
1022 let face = MemoryFace {
1023 font_match: rust_fontconfig::FontMatch {
1024 id,
1025 unicode_ranges,
1026 fallbacks: Vec::new(),
1027 },
1028 weight: style.weight,
1029 italic: style.italic,
1030 oblique: style.oblique,
1031 stretch: style.stretch,
1032 weight_axis: style.weight_axis,
1033 };
1034 let faces = self.memory_families.entry(norm.to_string()).or_default();
1035 if let Some(slot) = faces.iter_mut().find(|f| f.font_match.id == id) {
1036 *slot = face;
1037 } else {
1038 faces.push(face);
1039 }
1040 }
1041
1042 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1053 fn register_variable_instances(
1054 &mut self,
1055 norm: &str,
1056 family: &str,
1057 bytes: &[u8],
1058 coverage: &[UnicodeRange],
1059 min: f32,
1060 def: f32,
1061 max: f32,
1062 ) -> Option<FontId> {
1063 let base = parse_face_style(bytes, family);
1064 let hash = {
1065 use core::hash::Hasher;
1066 let mut h = DefaultHasher::new();
1067 h.write(bytes);
1068 h.finish()
1069 };
1070 let def_bucket = FcWeight::from_u16(def.round().clamp(1.0, 1000.0) as u16);
1071
1072 if let Some(cached) = self.vf_bake_cache.get(&hash).cloned() {
1074 for (id, style) in &cached {
1075 self.index_memory_face(norm, *id, coverage.to_vec(), style);
1076 }
1077 return cached
1078 .iter()
1079 .find(|(_, s)| s.weight == def_bucket)
1080 .or_else(|| cached.first())
1081 .map(|(id, _)| *id);
1082 }
1083
1084 let lo = min.round().clamp(1.0, 1000.0) as u16;
1085 let hi = max.round().clamp(1.0, 1000.0) as u16;
1086 let mut baked: Vec<(FontId, FaceStyle)> = Vec::new();
1087 for w in [100u16, 200, 300, 400, 500, 600, 700, 800, 900] {
1088 if w < lo || w > hi {
1089 continue;
1090 }
1091 let Some(inst_bytes) = crate::font::parsed::bake_weight_instance(bytes, 0, f32::from(w))
1092 else {
1093 continue;
1094 };
1095 let style = FaceStyle {
1096 weight: FcWeight::from_u16(w),
1097 italic: base.italic,
1098 oblique: base.oblique,
1099 stretch: base.stretch,
1100 weight_axis: None,
1101 };
1102 let pattern = rust_fontconfig::FcPattern {
1103 name: Some(family.to_string()),
1104 family: Some(family.to_string()),
1105 italic: if style.italic { PatternMatch::True } else { PatternMatch::False },
1106 oblique: if style.oblique { PatternMatch::True } else { PatternMatch::False },
1107 bold: if style.weight >= FcWeight::Bold { PatternMatch::True } else { PatternMatch::False },
1108 weight: style.weight,
1109 stretch: style.stretch,
1110 unicode_ranges: coverage.to_vec(),
1111 ..Default::default()
1112 };
1113 let id = FontId::new();
1114 self.fc_cache.with_memory_font_with_id(
1115 id,
1116 pattern,
1117 rust_fontconfig::FcFont {
1118 bytes: inst_bytes,
1119 font_index: 0,
1120 id: family.to_string(),
1121 },
1122 );
1123 self.index_memory_face(norm, id, coverage.to_vec(), &style);
1124 baked.push((id, style));
1125 }
1126
1127 if baked.is_empty() {
1128 return None;
1129 }
1130 let default_id = baked
1131 .iter()
1132 .find(|(_, s)| s.weight == def_bucket)
1133 .or_else(|| baked.first())
1134 .map(|(id, _)| *id)
1135 .unwrap();
1136 self.vf_bake_cache.insert(hash, baked);
1137 Some(default_id)
1138 }
1139
1140 pub fn register_builtin_mock_fonts(&mut self) {
1146 for (family, bytes) in crate::text3::mock_fonts::BUILTIN_MOCK_FONTS {
1147 self.register_named_font(
1148 family,
1149 bytes,
1150 crate::text3::mock_fonts::mock_font_ranges(),
1151 );
1152 }
1153 }
1154
1155 pub fn from_shared(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
1165 Self::new(fc_cache)
1166 }
1167
1168 pub fn from_arc_shared(
1177 fc_cache: FcFontCache,
1178 parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
1179 ) -> Result<Self, LayoutError> {
1180 let mut fm = Self {
1181 fc_cache,
1182 parsed_fonts,
1183 font_chain_cache: HashMap::new(),
1184 embedded_fonts: Mutex::new(HashMap::new()),
1185 font_hash_to_families: HashMap::new(),
1186 registry: None,
1187 last_resolved_font_stacks_sig: None,
1188 memory_families: HashMap::new(),
1189 vf_bake_cache: HashMap::new(),
1190 };
1191 fm.register_builtin_mock_fonts();
1192 Ok(fm)
1193 }
1194
1195 #[must_use]
1199 pub fn with_registry(
1200 mut self,
1201 registry: Arc<rust_fontconfig::registry::FcFontRegistry>,
1202 ) -> Self {
1203 self.registry = Some(registry);
1204 self
1205 }
1206
1207 pub fn shared_parsed_fonts(&self) -> Arc<Mutex<HashMap<FontId, T>>> {
1212 Arc::clone(&self.parsed_fonts)
1213 }
1214
1215 pub fn set_font_chain_cache(
1220 &mut self,
1221 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
1222 ) {
1223 self.font_chain_cache = chains;
1224 self.last_resolved_font_stacks_sig = None;
1225 }
1226
1227 pub fn set_font_chain_cache_with_sig(
1233 &mut self,
1234 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
1235 sig: Option<u64>,
1236 ) {
1237 self.font_chain_cache = chains;
1241 self.last_resolved_font_stacks_sig = sig;
1242 }
1243
1244 pub fn merge_font_chain_cache(
1248 &mut self,
1249 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
1250 ) {
1251 self.font_chain_cache.extend(chains);
1252 }
1253
1254 pub const fn get_font_chain_cache(
1256 &self,
1257 ) -> &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain> {
1258 &self.font_chain_cache
1259 }
1260
1261 pub fn get_embedded_font_by_hash(&self, font_hash: u64) -> Option<azul_css::props::basic::FontRef> {
1267 let embedded = self.embedded_fonts.lock().unwrap();
1268 embedded.get(&font_hash).cloned()
1269 }
1270
1271 pub fn get_font_by_hash(&self, font_hash: u64) -> Option<T> {
1277 let parsed = self.parsed_fonts.lock().unwrap();
1278 let found = parsed
1280 .iter()
1281 .find(|(_, font)| font.get_hash() == font_hash)
1282 .map(|(_, font)| font.clone());
1283 drop(parsed);
1284 found
1285 }
1286
1287 pub fn register_embedded_font(&self, font_ref: &azul_css::props::basic::FontRef) {
1293 let hash = font_ref.get_hash();
1294 let mut embedded = self.embedded_fonts.lock().unwrap();
1295 embedded.insert(hash, font_ref.clone());
1296 }
1297
1298 pub fn get_loaded_fonts(&self) -> LoadedFonts<T> {
1308 let parsed = self.parsed_fonts.lock().unwrap();
1309 parsed
1310 .iter()
1311 .map(|(id, font)| (*id, font.shallow_clone()))
1312 .collect()
1313 }
1314
1315 pub fn get_loaded_font_ids(&self) -> HashSet<FontId> {
1323 let parsed = self.parsed_fonts.lock().unwrap();
1324 if parsed.is_empty() {
1329 return HashSet::new();
1330 }
1331 unsafe { crate::az_mark(0x60788, 0xA1) };
1332 let out = parsed.keys().copied().collect();
1333 drop(parsed);
1334 unsafe { crate::az_mark(0x6078C, 0xA2) };
1335 out
1336 }
1337
1338 pub fn insert_font(&self, font_id: FontId, font: T) -> Option<T> {
1345 let mut parsed = self.parsed_fonts.lock().unwrap();
1346 parsed.insert(font_id, font)
1347 }
1348
1349 pub fn insert_fonts(&self, fonts: impl IntoIterator<Item = (FontId, T)>) {
1357 let mut parsed = self.parsed_fonts.lock().unwrap();
1358 for (font_id, font) in fonts {
1359 parsed.insert(font_id, font);
1360 }
1361 }
1362
1363 pub fn load_missing_for_chains<F>(
1375 &self,
1376 chains: &crate::solver3::getters::ResolvedFontChains,
1377 load_fn: F,
1378 ) -> Vec<(FontId, String)>
1379 where
1380 F: Fn(Arc<rust_fontconfig::FontBytes>, usize) -> Result<T, LayoutError>,
1381 {
1382 use crate::solver3::getters::{
1383 collect_font_ids_from_chains, compute_fonts_to_load, load_fonts_from_disk,
1384 };
1385 let required = collect_font_ids_from_chains(chains);
1386 let already = self.get_loaded_font_ids();
1387 let to_load = compute_fonts_to_load(&required, &already);
1388 if to_load.is_empty() {
1389 return Vec::new();
1390 }
1391 let result = load_fonts_from_disk(&to_load, &self.fc_cache, load_fn);
1392 self.insert_fonts(result.loaded);
1393 result.failed
1394 }
1395
1396 pub fn replace_fc_cache(&mut self, fc_cache: FcFontCache) {
1406 self.fc_cache = fc_cache;
1407 self.register_builtin_mock_fonts();
1408 }
1409
1410 pub fn remove_font(&self, font_id: &FontId) -> Option<T> {
1417 let mut parsed = self.parsed_fonts.lock().unwrap();
1418 parsed.remove(font_id)
1419 }
1420
1421 pub fn garbage_collect_fonts(
1442 &mut self,
1443 keep_ids: &HashSet<FontId>,
1444 keep_hashes: &HashSet<u64>,
1445 ) -> usize {
1446 let evicted = {
1447 let mut parsed = self.parsed_fonts.lock().unwrap();
1448 let before = parsed.len();
1449 parsed.retain(|id, _| keep_ids.contains(id));
1450 before.saturating_sub(parsed.len())
1451 };
1452 self.font_hash_to_families
1453 .retain(|h, _| keep_hashes.contains(h));
1454 evicted
1455 }
1456}
1457
1458#[derive(Debug, thiserror::Error)]
1464#[repr(C, u8)]
1465pub enum LayoutError {
1466 #[error("Bidi analysis failed: {0}")]
1467 BidiError(String),
1468 #[error("Shaping failed: {0}")]
1469 ShapingError(String),
1470 #[error("Font not found: {0:?}")]
1471 FontNotFound(FontSelector),
1472 #[error("Invalid text input: {0}")]
1473 InvalidText(String),
1474 #[error("Hyphenation failed: {0}")]
1475 HyphenationError(String),
1476}
1477
1478#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1480pub enum TextBoundary {
1481 Top,
1483 Bottom,
1485 Start,
1487 End,
1489}
1490
1491#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1493pub(crate) struct CursorBoundsError {
1494 pub(crate) boundary: TextBoundary,
1495 pub(crate) cursor: TextCursor,
1496}
1497
1498#[derive(Debug, Clone)]
1569pub struct UnifiedConstraints {
1570 pub shape_boundaries: Vec<ShapeBoundary>,
1572 pub shape_exclusions: Vec<ShapeBoundary>,
1573
1574 pub available_width: AvailableSpace,
1576 pub available_height: Option<f32>,
1577
1578 pub writing_mode: Option<WritingMode>,
1580 pub direction: Option<BidiDirection>,
1583 pub text_orientation: TextOrientation,
1584 pub text_align: TextAlign,
1585 pub text_justify: JustifyContent,
1586 pub line_height: LineHeight,
1588 pub vertical_align: VerticalAlign,
1589 pub strut_ascent: f32,
1591 pub strut_descent: f32,
1592 pub strut_x_height: f32,
1594
1595 pub ch_width: f32,
1598
1599 pub overflow: OverflowBehavior,
1601 pub segment_alignment: SegmentAlignment,
1602
1603 pub text_combine_upright: Option<TextCombineUpright>,
1605 pub exclusion_margin: f32,
1606 pub hyphenation: Hyphens,
1607 pub hyphenation_language: Option<Language>,
1608 pub text_indent: f32,
1609 pub text_indent_each_line: bool,
1610 pub text_indent_hanging: bool,
1611 pub initial_letter: Option<InitialLetter>,
1612 pub line_clamp: Option<NonZeroUsize>,
1613
1614 pub text_wrap: TextWrap,
1616 pub columns: u32,
1617 pub column_gap: f32,
1618 pub hanging_punctuation: bool,
1619 pub overflow_wrap: OverflowWrap,
1620 pub text_align_last: TextAlign,
1621 pub word_break: WordBreak,
1623 pub white_space_mode: WhiteSpaceMode,
1624 pub line_break: LineBreakStrictness,
1625 pub unicode_bidi: UnicodeBidi,
1627}
1628
1629impl Default for UnifiedConstraints {
1630 fn default() -> Self {
1631 Self {
1632 shape_boundaries: Vec::new(),
1633 shape_exclusions: Vec::new(),
1634
1635 available_width: AvailableSpace::MaxContent,
1642 available_height: None,
1643 writing_mode: None,
1644 direction: None, text_orientation: TextOrientation::default(),
1646 text_align: TextAlign::default(),
1647 text_justify: JustifyContent::default(),
1648 line_height: LineHeight::Normal,
1649 vertical_align: VerticalAlign::default(),
1650 strut_ascent: DEFAULT_STRUT_ASCENT,
1651 strut_descent: DEFAULT_STRUT_DESCENT,
1652 strut_x_height: DEFAULT_X_HEIGHT,
1653 ch_width: DEFAULT_CH_WIDTH,
1654 overflow: OverflowBehavior::default(),
1655 segment_alignment: SegmentAlignment::default(),
1656 text_combine_upright: None,
1657 exclusion_margin: 0.0,
1658 hyphenation: Hyphens::default(),
1659 hyphenation_language: None,
1660 columns: 1,
1661 column_gap: 0.0,
1662 hanging_punctuation: false,
1663 text_indent: 0.0,
1664 text_indent_each_line: false,
1665 text_indent_hanging: false,
1666 initial_letter: None,
1667 line_clamp: None,
1668 text_wrap: TextWrap::default(),
1669 overflow_wrap: OverflowWrap::default(),
1670 text_align_last: TextAlign::default(),
1671 word_break: WordBreak::default(),
1672 white_space_mode: WhiteSpaceMode::default(),
1673 line_break: LineBreakStrictness::default(),
1674 unicode_bidi: UnicodeBidi::default(),
1675 }
1676 }
1677}
1678
1679impl Hash for UnifiedConstraints {
1681 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
1683 self.shape_boundaries.hash(state);
1684 self.shape_exclusions.hash(state);
1685 self.available_width.hash(state);
1686 self.available_height
1687 .map(|h| h.round() as isize)
1688 .hash(state);
1689 self.writing_mode.hash(state);
1690 self.direction.hash(state);
1691 self.text_orientation.hash(state);
1692 self.text_align.hash(state);
1693 self.text_justify.hash(state);
1694 self.line_height.hash(state);
1695 self.vertical_align.hash(state);
1696 (self.strut_ascent.round() as isize).hash(state);
1697 (self.strut_descent.round() as isize).hash(state);
1698 (self.strut_x_height.round() as isize).hash(state);
1699 (self.ch_width.round() as isize).hash(state);
1700 self.overflow.hash(state);
1701 self.segment_alignment.hash(state);
1702 self.text_combine_upright.hash(state);
1703 (self.exclusion_margin.round() as isize).hash(state);
1704 self.hyphenation.hash(state);
1705 self.hyphenation_language.hash(state);
1706 (self.text_indent.round() as isize).hash(state);
1707 self.text_indent_each_line.hash(state);
1708 self.text_indent_hanging.hash(state);
1709 self.initial_letter.hash(state);
1710 self.line_clamp.hash(state);
1711 self.columns.hash(state);
1712 (self.column_gap.round() as isize).hash(state);
1713 self.hanging_punctuation.hash(state);
1714 self.overflow_wrap.hash(state);
1715 self.text_align_last.hash(state);
1716 self.word_break.hash(state);
1717 self.white_space_mode.hash(state);
1718 self.line_break.hash(state);
1719 self.unicode_bidi.hash(state);
1720 }
1721}
1722
1723impl PartialEq for UnifiedConstraints {
1724 fn eq(&self, other: &Self) -> bool {
1725 self.shape_boundaries == other.shape_boundaries
1726 && self.shape_exclusions == other.shape_exclusions
1727 && self.available_width == other.available_width
1728 && match (self.available_height, other.available_height) {
1729 (None, None) => true,
1730 (Some(h1), Some(h2)) => round_eq(h1, h2),
1731 _ => false,
1732 }
1733 && self.writing_mode == other.writing_mode
1734 && self.direction == other.direction
1735 && self.text_orientation == other.text_orientation
1736 && self.text_align == other.text_align
1737 && self.text_justify == other.text_justify
1738 && self.line_height == other.line_height
1739 && self.vertical_align == other.vertical_align
1740 && round_eq(self.strut_ascent, other.strut_ascent)
1741 && round_eq(self.strut_descent, other.strut_descent)
1742 && round_eq(self.strut_x_height, other.strut_x_height)
1743 && round_eq(self.ch_width, other.ch_width)
1744 && self.overflow == other.overflow
1745 && self.segment_alignment == other.segment_alignment
1746 && self.text_combine_upright == other.text_combine_upright
1747 && round_eq(self.exclusion_margin, other.exclusion_margin)
1748 && self.hyphenation == other.hyphenation
1749 && self.hyphenation_language == other.hyphenation_language
1750 && round_eq(self.text_indent, other.text_indent)
1751 && self.text_indent_each_line == other.text_indent_each_line
1752 && self.text_indent_hanging == other.text_indent_hanging
1753 && self.initial_letter == other.initial_letter
1754 && self.line_clamp == other.line_clamp
1755 && self.columns == other.columns
1756 && round_eq(self.column_gap, other.column_gap)
1757 && self.hanging_punctuation == other.hanging_punctuation
1758 && self.overflow_wrap == other.overflow_wrap
1759 && self.text_align_last == other.text_align_last
1760 && self.word_break == other.word_break
1761 && self.white_space_mode == other.white_space_mode
1762 && self.line_break == other.line_break
1763 && self.unicode_bidi == other.unicode_bidi
1764 }
1765}
1766
1767impl Eq for UnifiedConstraints {}
1768
1769impl UnifiedConstraints {
1770 #[must_use] pub fn resolved_line_height(&self) -> f32 {
1773 match self.line_height {
1774 LineHeight::Normal => self.strut_ascent + self.strut_descent,
1782 LineHeight::Px(px) => px,
1783 }
1784 }
1785 fn direction(&self, fallback: BidiDirection) -> BidiDirection {
1786 self.writing_mode.map_or(fallback, |s| s.get_direction().unwrap_or(fallback))
1787 }
1788 const fn is_vertical(&self) -> bool {
1789 matches!(
1790 self.writing_mode,
1791 Some(WritingMode::VerticalRl | WritingMode::VerticalLr)
1792 )
1793 }
1794}
1795
1796#[derive(Debug, Clone)]
1798pub struct LineConstraints {
1799 pub segments: Vec<LineSegment>,
1800 pub total_available: f32,
1801 pub is_min_content: bool,
1805}
1806
1807impl WritingMode {
1808 #[allow(clippy::trivially_copy_pass_by_ref)] #[allow(clippy::match_same_arms)] const fn get_direction(&self) -> Option<BidiDirection> {
1811 match self {
1812 Self::HorizontalTb => None,
1814 Self::VerticalRl => Some(BidiDirection::Rtl),
1815 Self::VerticalLr => Some(BidiDirection::Ltr),
1816 Self::SidewaysRl => Some(BidiDirection::Rtl),
1817 Self::SidewaysLr => Some(BidiDirection::Ltr),
1818 }
1819 }
1820}
1821
1822#[derive(Debug, Clone, Hash)]
1824pub struct StyledRun {
1825 pub text: String,
1826 pub style: Arc<StyleProperties>,
1827 pub logical_start_byte: usize,
1829 pub source_node_id: Option<NodeId>,
1832}
1833
1834#[derive(Debug, Clone)]
1836pub struct VisualRun<'a> {
1837 pub text_slice: &'a str,
1838 pub style: Arc<StyleProperties>,
1839 pub logical_start_byte: usize,
1840 pub bidi_level: BidiLevel,
1841 pub script: Script,
1842 pub language: Language,
1843}
1844
1845#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1850pub struct FontSelector {
1851 pub family: String,
1852 pub weight: FcWeight,
1853 pub style: FontStyle,
1854 pub unicode_ranges: Vec<UnicodeRange>,
1855}
1856
1857impl Default for FontSelector {
1858 fn default() -> Self {
1859 Self {
1860 family: "serif".to_string(),
1861 weight: FcWeight::Normal,
1862 style: FontStyle::Normal,
1863 unicode_ranges: Vec::new(),
1864 }
1865 }
1866}
1867
1868#[derive(Debug, Clone)]
1878#[repr(C, u8)]
1879pub enum FontStack {
1880 Stack(Vec<FontSelector>),
1883 Ref(azul_css::props::basic::font::FontRef),
1886}
1887
1888impl Default for FontStack {
1889 fn default() -> Self {
1890 Self::Stack(vec![FontSelector::default()])
1891 }
1892}
1893
1894impl FontStack {
1895 #[must_use] pub const fn is_ref(&self) -> bool {
1897 matches!(self, Self::Ref(_))
1898 }
1899
1900 #[must_use] pub const fn as_ref(&self) -> Option<&azul_css::props::basic::font::FontRef> {
1902 match self {
1903 Self::Ref(r) => Some(r),
1904 Self::Stack(_) => None,
1905 }
1906 }
1907
1908 #[must_use] pub fn as_stack(&self) -> Option<&[FontSelector]> {
1910 match self {
1911 Self::Stack(s) => Some(s),
1912 Self::Ref(_) => None,
1913 }
1914 }
1915
1916 #[must_use] pub fn first_selector(&self) -> Option<&FontSelector> {
1918 match self {
1919 Self::Stack(s) => s.first(),
1920 Self::Ref(_) => None,
1921 }
1922 }
1923
1924 #[must_use] pub fn first_family(&self) -> &str {
1926 match self {
1927 Self::Stack(s) => s.first().map_or("serif", |f| f.family.as_str()),
1928 Self::Ref(_) => "<embedded-font>",
1929 }
1930 }
1931}
1932
1933impl PartialEq for FontStack {
1934 fn eq(&self, other: &Self) -> bool {
1935 match (self, other) {
1936 (Self::Stack(a), Self::Stack(b)) => a == b,
1937 (Self::Ref(a), Self::Ref(b)) => a.parsed == b.parsed,
1938 _ => false,
1939 }
1940 }
1941}
1942
1943impl Eq for FontStack {}
1944
1945impl Hash for FontStack {
1946 fn hash<H: Hasher>(&self, state: &mut H) {
1947 discriminant(self).hash(state);
1948 match self {
1949 Self::Stack(s) => s.hash(state),
1950 Self::Ref(r) => (r.parsed as usize).hash(state),
1951 }
1952 }
1953}
1954
1955#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1959pub struct FontHash {
1960 pub font_hash: u64,
1962}
1963
1964impl FontHash {
1965 #[must_use] pub const fn invalid() -> Self {
1966 Self { font_hash: 0 }
1967 }
1968
1969 #[must_use] pub const fn from_hash(font_hash: u64) -> Self {
1970 Self { font_hash }
1971 }
1972}
1973
1974#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1975pub enum FontStyle {
1976 Normal,
1977 Italic,
1978 Oblique,
1979}
1980
1981#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1983pub enum SegmentAlignment {
1984 #[default]
1986 First,
1987 Total,
1990}
1991
1992#[derive(Copy, Debug, Clone)]
1993pub struct VerticalMetrics {
1994 pub advance: f32,
1995 pub bearing_x: f32,
1996 pub bearing_y: f32,
1997 pub origin_y: f32,
1998}
1999
2000#[derive(Copy, Debug, Clone)]
2012pub struct LayoutFontMetrics {
2013 pub ascent: f32,
2014 pub descent: f32,
2015 pub line_gap: f32,
2016 pub units_per_em: u16,
2017 pub x_height: Option<f32>,
2020 pub cap_height: Option<f32>,
2023}
2024
2025impl LayoutFontMetrics {
2026 #[must_use] pub fn baseline_scaled(&self, font_size: f32) -> f32 {
2030 let scale = font_size / f32::from(self.units_per_em);
2031 self.ascent * scale
2032 }
2033
2034 #[must_use] pub fn x_height_scaled(&self, font_size: f32) -> f32 {
2037 let scale = font_size / f32::from(self.units_per_em);
2038 self.x_height.map_or(font_size * 0.5, |xh| xh * scale)
2039 }
2040
2041 #[must_use] pub fn cap_height_scaled(&self, font_size: f32) -> f32 {
2044 let scale = font_size / f32::from(self.units_per_em);
2045 self.cap_height.unwrap_or(self.ascent) * scale
2046 }
2047
2048 #[must_use] pub fn from_font_metrics(metrics: &azul_css::props::basic::FontMetrics) -> Self {
2068 let ascent = metrics.s_typo_ascender
2069 .as_option()
2070 .map_or_else(|| f32::from(metrics.ascender), |v| f32::from(*v));
2071 let descent = metrics.s_typo_descender
2072 .as_option()
2073 .map_or_else(|| f32::from(metrics.descender), |v| f32::from(*v));
2074 let line_gap = metrics.s_typo_line_gap
2077 .as_option()
2078 .map_or_else(|| f32::from(metrics.line_gap), |v| f32::from(*v))
2079 .max(0.0);
2080 let x_height = metrics.sx_height
2081 .as_option()
2082 .map(|v| f32::from(*v));
2083 let cap_height = metrics.s_cap_height
2084 .as_option()
2085 .map(|v| f32::from(*v));
2086 Self {
2087 ascent,
2088 descent,
2089 line_gap,
2090 units_per_em: metrics.units_per_em,
2091 x_height,
2092 cap_height,
2093 }
2094 }
2095
2096 #[must_use] pub fn em_over(&self) -> f32 {
2101 let central = self.central_baseline();
2102 central + (f32::from(self.units_per_em) / 2.0)
2103 }
2104
2105 #[must_use] pub fn em_under(&self) -> f32 {
2108 let central = self.central_baseline();
2109 central - (f32::from(self.units_per_em) / 2.0)
2110 }
2111
2112 #[must_use] pub const fn central_baseline(&self) -> f32 {
2115 f32::midpoint(self.ascent, self.descent)
2116 }
2117}
2118
2119#[derive(Copy, Debug, Clone)]
2120pub struct LineSegment {
2121 pub start_x: f32,
2122 pub width: f32,
2123 pub priority: u8,
2125}
2126
2127#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2128pub enum TextWrap {
2129 #[default]
2130 Wrap,
2131 Balance,
2132 NoWrap,
2133}
2134
2135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
2140pub enum OverflowWrap {
2141 #[default]
2143 Normal,
2144 Anywhere,
2148 BreakWord,
2152}
2153
2154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
2158pub enum Hyphens {
2159 None,
2161 #[default]
2163 Manual,
2164 Auto,
2166}
2167
2168#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2172pub enum WhiteSpaceMode {
2173 #[default]
2174 Normal,
2175 Nowrap,
2176 Pre,
2177 PreWrap,
2178 PreLine,
2179 BreakSpaces,
2180}
2181
2182#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2189pub enum LineBreakStrictness {
2190 #[default]
2191 Auto,
2192 Loose,
2193 Normal,
2194 Strict,
2195 Anywhere,
2198}
2199
2200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
2202pub enum WordBreak {
2203 #[default]
2206 Normal,
2207 BreakAll,
2209 KeepAll,
2212}
2213
2214#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
2222pub struct InitialLetter {
2223 pub size: f32,
2225 pub sink: u32,
2228 pub count: NonZeroUsize,
2230 pub align: InitialLetterAlign,
2234}
2235
2236#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2239pub enum InitialLetterAlign {
2240 Auto,
2242 Alphabetic,
2244 Hanging,
2246 Ideographic,
2248}
2249
2250impl Eq for InitialLetter {}
2255
2256impl Hash for InitialLetter {
2257 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
2259 (self.size.round() as isize).hash(state);
2264 self.sink.hash(state);
2265 self.count.hash(state);
2266 self.align.hash(state);
2267 }
2268}
2269
2270#[derive(Copy, Debug, Clone, PartialOrd)]
2272pub enum PathSegment {
2273 MoveTo(Point),
2274 LineTo(Point),
2275 CurveTo {
2276 control1: Point,
2277 control2: Point,
2278 end: Point,
2279 },
2280 QuadTo {
2281 control: Point,
2282 end: Point,
2283 },
2284 Arc {
2285 center: Point,
2286 radius: f32,
2287 start_angle: f32,
2288 end_angle: f32,
2289 },
2290 Close,
2291}
2292
2293impl Hash for PathSegment {
2295 #[allow(clippy::cast_possible_truncation)] #[allow(clippy::match_same_arms)] fn hash<H: Hasher>(&self, state: &mut H) {
2298 discriminant(self).hash(state);
2300
2301 match self {
2302 Self::MoveTo(p) => p.hash(state),
2303 Self::LineTo(p) => p.hash(state),
2304 Self::CurveTo {
2305 control1,
2306 control2,
2307 end,
2308 } => {
2309 control1.hash(state);
2310 control2.hash(state);
2311 end.hash(state);
2312 }
2313 Self::QuadTo { control, end } => {
2314 control.hash(state);
2315 end.hash(state);
2316 }
2317 Self::Arc {
2318 center,
2319 radius,
2320 start_angle,
2321 end_angle,
2322 } => {
2323 center.hash(state);
2324 (radius.round() as isize).hash(state);
2325 (start_angle.round() as isize).hash(state);
2326 (end_angle.round() as isize).hash(state);
2327 }
2328 Self::Close => {} }
2330 }
2331}
2332
2333impl PartialEq for PathSegment {
2334 #[allow(clippy::similar_names)] #[allow(clippy::match_same_arms)] fn eq(&self, other: &Self) -> bool {
2337 match (self, other) {
2338 (Self::MoveTo(a), Self::MoveTo(b)) => a == b,
2339 (Self::LineTo(a), Self::LineTo(b)) => a == b,
2340 (
2341 Self::CurveTo {
2342 control1: c1a,
2343 control2: c2a,
2344 end: ea,
2345 },
2346 Self::CurveTo {
2347 control1: c1b,
2348 control2: c2b,
2349 end: eb,
2350 },
2351 ) => c1a == c1b && c2a == c2b && ea == eb,
2352 (
2353 Self::QuadTo {
2354 control: ca,
2355 end: ea,
2356 },
2357 Self::QuadTo {
2358 control: cb,
2359 end: eb,
2360 },
2361 ) => ca == cb && ea == eb,
2362 (
2363 Self::Arc {
2364 center: ca,
2365 radius: ra,
2366 start_angle: sa_a,
2367 end_angle: ea_a,
2368 },
2369 Self::Arc {
2370 center: cb,
2371 radius: rb,
2372 start_angle: sa_b,
2373 end_angle: ea_b,
2374 },
2375 ) => ca == cb && round_eq(*ra, *rb) && round_eq(*sa_a, *sa_b) && round_eq(*ea_a, *ea_b),
2376 (Self::Close, Self::Close) => true,
2377 _ => false, }
2379 }
2380}
2381
2382impl Eq for PathSegment {}
2383
2384#[derive(Debug, Clone, Hash)]
2393#[repr(C, u8)]
2394pub enum InlineContent {
2395 Text(StyledRun),
2396 Image(InlineImage),
2397 Shape(InlineShape),
2398 Space(InlineSpace),
2399 LineBreak(InlineBreak),
2400 Tab {
2402 style: Arc<StyleProperties>,
2403 },
2404 Marker {
2408 run: StyledRun,
2409 position_outside: bool,
2411 },
2412 Ruby {
2414 base: Vec<InlineContent>,
2415 text: Vec<InlineContent>,
2416 style: Arc<StyleProperties>,
2418 },
2419}
2420
2421#[derive(Debug, Clone)]
2422pub struct InlineImage {
2423 pub source: ImageSource,
2424 pub intrinsic_size: Size,
2425 pub display_size: Option<Size>,
2426 pub baseline_offset: f32,
2428 pub alignment: VerticalAlign,
2429 pub object_fit: ObjectFit,
2430}
2431
2432impl PartialEq for InlineImage {
2433 fn eq(&self, other: &Self) -> bool {
2434 self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
2435 && self.source == other.source
2436 && self.intrinsic_size == other.intrinsic_size
2437 && self.display_size == other.display_size
2438 && self.alignment == other.alignment
2439 && self.object_fit == other.object_fit
2440 }
2441}
2442
2443impl Eq for InlineImage {}
2444
2445impl Hash for InlineImage {
2446 fn hash<H: Hasher>(&self, state: &mut H) {
2447 self.source.hash(state);
2448 self.intrinsic_size.hash(state);
2449 self.display_size.hash(state);
2450 self.baseline_offset.to_bits().hash(state);
2451 self.alignment.hash(state);
2452 self.object_fit.hash(state);
2453 }
2454}
2455
2456impl PartialOrd for InlineImage {
2457 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2458 Some(self.cmp(other))
2459 }
2460}
2461
2462impl Ord for InlineImage {
2463 fn cmp(&self, other: &Self) -> Ordering {
2464 self.source
2465 .cmp(&other.source)
2466 .then_with(|| self.intrinsic_size.cmp(&other.intrinsic_size))
2467 .then_with(|| self.display_size.cmp(&other.display_size))
2468 .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
2469 .then_with(|| self.alignment.cmp(&other.alignment))
2470 .then_with(|| self.object_fit.cmp(&other.object_fit))
2471 }
2472}
2473
2474#[derive(Debug, Clone)]
2476pub struct Glyph {
2477 pub glyph_id: u16,
2479 pub codepoint: char,
2480 pub font_hash: u64,
2482 pub font_metrics: LayoutFontMetrics,
2484 pub style: Arc<StyleProperties>,
2485 pub source: GlyphSource,
2486
2487 pub logical_byte_index: usize,
2489 pub logical_byte_len: usize,
2490 pub content_index: usize,
2491 pub cluster: u32,
2492
2493 pub advance: f32,
2495 pub kerning: f32,
2496 pub offset: Point,
2497
2498 pub vertical_advance: f32,
2500 pub vertical_origin_y: f32, pub vertical_bearing: Point,
2502 pub orientation: GlyphOrientation,
2503
2504 pub script: Script,
2506 pub bidi_level: BidiLevel,
2507}
2508
2509impl Glyph {
2510 #[inline]
2511 fn bounds(&self) -> Rect {
2512 Rect {
2513 x: 0.0,
2514 y: 0.0,
2515 width: self.advance,
2516 height: self.style.line_height.resolve_with_metrics(self.style.font_size_px, &self.font_metrics),
2517 }
2518 }
2519
2520 #[inline]
2521 const fn character_class(&self) -> CharacterClass {
2522 classify_character(self.codepoint as u32)
2523 }
2524
2525 #[inline]
2526 fn is_whitespace(&self) -> bool {
2527 self.character_class() == CharacterClass::Space
2528 }
2529
2530 #[inline]
2531 fn can_justify(&self) -> bool {
2532 !self.codepoint.is_whitespace() && self.character_class() != CharacterClass::Combining
2533 }
2534
2535 #[inline]
2536 const fn justification_priority(&self) -> u8 {
2537 get_justification_priority(self.character_class())
2538 }
2539
2540 #[inline]
2541 const fn break_opportunity_after(&self) -> bool {
2542 let is_whitespace = self.codepoint.is_whitespace();
2543 let is_soft_hyphen = self.codepoint == '\u{00AD}';
2544 let is_hyphen_minus = self.codepoint == '\u{002D}';
2545 let is_hyphen = self.codepoint == '\u{2010}';
2546 is_whitespace || is_soft_hyphen || is_hyphen_minus || is_hyphen
2547 }
2548}
2549
2550#[derive(Debug, Clone)]
2552pub(crate) struct TextRunInfo<'a> {
2553 pub(crate) text: &'a str,
2554 pub(crate) style: Arc<StyleProperties>,
2555 pub(crate) logical_start: usize,
2556 pub(crate) content_index: usize,
2557}
2558
2559#[derive(Debug, Clone)]
2560pub enum ImageSource {
2561 Ref(ImageRef),
2563 Url(String),
2565 Data(Arc<[u8]>),
2567 Svg(Arc<str>),
2569 Placeholder(Size),
2571}
2572
2573impl PartialEq for ImageSource {
2574 fn eq(&self, other: &Self) -> bool {
2575 match (self, other) {
2576 (Self::Ref(a), Self::Ref(b)) => a.get_hash() == b.get_hash(),
2577 (Self::Url(a), Self::Url(b)) => a == b,
2578 (Self::Data(a), Self::Data(b)) => Arc::ptr_eq(a, b),
2579 (Self::Svg(a), Self::Svg(b)) => Arc::ptr_eq(a, b),
2580 (Self::Placeholder(a), Self::Placeholder(b)) => {
2581 a.width.to_bits() == b.width.to_bits() && a.height.to_bits() == b.height.to_bits()
2582 }
2583 _ => false,
2584 }
2585 }
2586}
2587
2588impl Eq for ImageSource {}
2589
2590impl Hash for ImageSource {
2591 fn hash<H: Hasher>(&self, state: &mut H) {
2592 discriminant(self).hash(state);
2593 match self {
2594 Self::Ref(r) => r.get_hash().hash(state),
2595 Self::Url(s) => s.hash(state),
2596 Self::Data(d) => (Arc::as_ptr(d).cast::<u8>() as usize).hash(state),
2597 Self::Svg(s) => (Arc::as_ptr(s).cast::<u8>() as usize).hash(state),
2598 Self::Placeholder(sz) => {
2599 sz.width.to_bits().hash(state);
2600 sz.height.to_bits().hash(state);
2601 }
2602 }
2603 }
2604}
2605
2606impl PartialOrd for ImageSource {
2607 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2608 Some(self.cmp(other))
2609 }
2610}
2611
2612impl Ord for ImageSource {
2613 fn cmp(&self, other: &Self) -> Ordering {
2614 const fn variant_index(s: &ImageSource) -> u8 {
2615 match s {
2616 ImageSource::Ref(_) => 0,
2617 ImageSource::Url(_) => 1,
2618 ImageSource::Data(_) => 2,
2619 ImageSource::Svg(_) => 3,
2620 ImageSource::Placeholder(_) => 4,
2621 }
2622 }
2623 match (self, other) {
2624 (Self::Ref(a), Self::Ref(b)) => a.get_hash().cmp(&b.get_hash()),
2625 (Self::Url(a), Self::Url(b)) => a.cmp(b),
2626 (Self::Data(a), Self::Data(b)) => {
2627 (Arc::as_ptr(a).cast::<u8>() as usize).cmp(&(Arc::as_ptr(b).cast::<u8>() as usize))
2628 }
2629 (Self::Svg(a), Self::Svg(b)) => {
2630 (Arc::as_ptr(a).cast::<u8>() as usize).cmp(&(Arc::as_ptr(b).cast::<u8>() as usize))
2631 }
2632 (Self::Placeholder(a), Self::Placeholder(b)) => {
2633 (a.width.to_bits(), a.height.to_bits())
2634 .cmp(&(b.width.to_bits(), b.height.to_bits()))
2635 }
2636 _ => variant_index(self).cmp(&variant_index(other)),
2638 }
2639 }
2640}
2641
2642#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
2648pub enum VerticalAlign {
2649 #[default]
2651 Baseline,
2652 Bottom,
2654 Top,
2656 Middle,
2658 TextTop,
2660 TextBottom,
2662 Sub,
2664 Super,
2666 Offset(f32),
2668}
2669
2670impl Hash for VerticalAlign {
2671 fn hash<H: Hasher>(&self, state: &mut H) {
2672 discriminant(self).hash(state);
2673 if let Self::Offset(f) = self {
2674 f.to_bits().hash(state);
2675 }
2676 }
2677}
2678
2679impl Eq for VerticalAlign {}
2680
2681#[allow(clippy::derive_ord_xor_partial_ord)]
2684impl Ord for VerticalAlign {
2685 fn cmp(&self, other: &Self) -> Ordering {
2686 self.partial_cmp(other).unwrap_or(Ordering::Equal)
2687 }
2688}
2689
2690#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
2691pub enum ObjectFit {
2692 Fill,
2694 Contain,
2696 Cover,
2698 None,
2700 ScaleDown,
2702}
2703
2704#[derive(Copy, Debug, Clone, PartialEq)]
2710pub struct InlineBorderInfo {
2711 pub top: f32,
2713 pub right: f32,
2714 pub bottom: f32,
2715 pub left: f32,
2716 pub top_color: ColorU,
2718 pub right_color: ColorU,
2719 pub bottom_color: ColorU,
2720 pub left_color: ColorU,
2721 pub radius: Option<f32>,
2723 pub padding_top: f32,
2725 pub padding_right: f32,
2726 pub padding_bottom: f32,
2727 pub padding_left: f32,
2728 pub is_first_fragment: bool,
2733 pub is_last_fragment: bool,
2735 pub is_rtl: bool,
2739}
2740
2741impl Default for InlineBorderInfo {
2742 fn default() -> Self {
2743 Self {
2744 top: 0.0,
2745 right: 0.0,
2746 bottom: 0.0,
2747 left: 0.0,
2748 top_color: ColorU::TRANSPARENT,
2749 right_color: ColorU::TRANSPARENT,
2750 bottom_color: ColorU::TRANSPARENT,
2751 left_color: ColorU::TRANSPARENT,
2752 radius: None,
2753 padding_top: 0.0,
2754 padding_right: 0.0,
2755 padding_bottom: 0.0,
2756 padding_left: 0.0,
2757 is_first_fragment: true,
2758 is_last_fragment: true,
2759 is_rtl: false,
2760 }
2761 }
2762}
2763
2764impl InlineBorderInfo {
2765 #[must_use] pub fn has_border(&self) -> bool {
2767 self.top > 0.0 || self.right > 0.0 || self.bottom > 0.0 || self.left > 0.0
2768 }
2769
2770 #[must_use] pub fn has_chrome(&self) -> bool {
2772 self.has_border()
2773 || self.padding_top > 0.0
2774 || self.padding_right > 0.0
2775 || self.padding_bottom > 0.0
2776 || self.padding_left > 0.0
2777 }
2778
2779 #[must_use] pub fn left_inset(&self) -> f32 {
2788 let show = if self.is_rtl { self.is_last_fragment } else { self.is_first_fragment };
2789 if show { self.left + self.padding_left } else { 0.0 }
2790 }
2791 #[must_use] pub fn right_inset(&self) -> f32 {
2794 let show = if self.is_rtl { self.is_first_fragment } else { self.is_last_fragment };
2795 if show { self.right + self.padding_right } else { 0.0 }
2796 }
2797 #[must_use] pub fn top_inset(&self) -> f32 { self.top + self.padding_top }
2799 #[must_use] pub fn bottom_inset(&self) -> f32 { self.bottom + self.padding_bottom }
2801}
2802
2803#[derive(Debug, Clone)]
2804pub struct InlineShape {
2805 pub shape_def: ShapeDefinition,
2806 pub fill: Option<ColorU>,
2807 pub stroke: Option<Stroke>,
2808 pub baseline_offset: f32,
2809 pub alignment: VerticalAlign,
2812 pub source_node_id: Option<NodeId>,
2816}
2817
2818#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
2819pub enum OverflowBehavior {
2820 Visible,
2822 Hidden,
2824 Scroll,
2826 #[default]
2828 Auto,
2829 Break,
2831}
2832
2833#[derive(Debug, Clone)]
2834pub(crate) struct MeasuredImage {
2835 pub(crate) source: ImageSource,
2836 pub(crate) size: Size,
2837 pub(crate) baseline_offset: f32,
2838 pub(crate) alignment: VerticalAlign,
2839 pub(crate) content_index: usize,
2840}
2841
2842#[derive(Debug, Clone)]
2843pub(crate) struct MeasuredShape {
2844 pub(crate) shape_def: ShapeDefinition,
2845 pub(crate) size: Size,
2846 pub(crate) baseline_offset: f32,
2847 pub(crate) alignment: VerticalAlign,
2848 pub(crate) content_index: usize,
2849}
2850
2851#[derive(Copy, Debug, Clone)]
2852pub struct InlineSpace {
2853 pub width: f32,
2854 pub is_breaking: bool, pub is_stretchy: bool, }
2857
2858impl PartialEq for InlineSpace {
2859 fn eq(&self, other: &Self) -> bool {
2860 self.width.to_bits() == other.width.to_bits()
2861 && self.is_breaking == other.is_breaking
2862 && self.is_stretchy == other.is_stretchy
2863 }
2864}
2865
2866impl Eq for InlineSpace {}
2867
2868impl Hash for InlineSpace {
2869 fn hash<H: Hasher>(&self, state: &mut H) {
2870 self.width.to_bits().hash(state);
2871 self.is_breaking.hash(state);
2872 self.is_stretchy.hash(state);
2873 }
2874}
2875
2876impl PartialOrd for InlineSpace {
2877 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2878 Some(self.cmp(other))
2879 }
2880}
2881
2882impl Ord for InlineSpace {
2883 fn cmp(&self, other: &Self) -> Ordering {
2884 self.width
2885 .total_cmp(&other.width)
2886 .then_with(|| self.is_breaking.cmp(&other.is_breaking))
2887 .then_with(|| self.is_stretchy.cmp(&other.is_stretchy))
2888 }
2889}
2890
2891impl PartialEq for InlineShape {
2892 fn eq(&self, other: &Self) -> bool {
2893 self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
2894 && self.shape_def == other.shape_def
2895 && self.fill == other.fill
2896 && self.stroke == other.stroke
2897 && self.alignment == other.alignment
2898 && self.source_node_id == other.source_node_id
2899 }
2900}
2901
2902impl Eq for InlineShape {}
2903
2904impl Hash for InlineShape {
2905 fn hash<H: Hasher>(&self, state: &mut H) {
2906 self.shape_def.hash(state);
2907 self.fill.hash(state);
2908 self.stroke.hash(state);
2909 self.baseline_offset.to_bits().hash(state);
2910 self.alignment.hash(state);
2911 self.source_node_id.hash(state);
2912 }
2913}
2914
2915impl PartialOrd for InlineShape {
2916 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2917 Some(
2918 self.shape_def
2919 .partial_cmp(&other.shape_def)?
2920 .then_with(|| self.fill.cmp(&other.fill))
2921 .then_with(|| {
2922 self.stroke
2923 .partial_cmp(&other.stroke)
2924 .unwrap_or(Ordering::Equal)
2925 })
2926 .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
2927 .then_with(|| self.alignment.cmp(&other.alignment))
2928 .then_with(|| self.source_node_id.cmp(&other.source_node_id)),
2929 )
2930 }
2931}
2932
2933#[derive(Debug, Default, Clone, Copy)]
2934pub struct Rect {
2935 pub x: f32,
2936 pub y: f32,
2937 pub width: f32,
2938 pub height: f32,
2939}
2940
2941impl PartialEq for Rect {
2942 fn eq(&self, other: &Self) -> bool {
2943 round_eq(self.x, other.x)
2944 && round_eq(self.y, other.y)
2945 && round_eq(self.width, other.width)
2946 && round_eq(self.height, other.height)
2947 }
2948}
2949impl Eq for Rect {}
2950
2951impl Hash for Rect {
2952 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
2954 (self.x.round() as isize).hash(state);
2957 (self.y.round() as isize).hash(state);
2958 (self.width.round() as isize).hash(state);
2959 (self.height.round() as isize).hash(state);
2960 }
2961}
2962
2963#[derive(Debug, Default, Clone, Copy)]
2964pub struct Size {
2965 pub width: f32,
2966 pub height: f32,
2967}
2968
2969impl PartialOrd for Size {
2970 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2971 Some(self.cmp(other))
2972 }
2973}
2974
2975impl Ord for Size {
2976 #[allow(clippy::cast_possible_truncation)] fn cmp(&self, other: &Self) -> Ordering {
2978 (self.width.round() as isize)
2979 .cmp(&(other.width.round() as isize))
2980 .then_with(|| (self.height.round() as isize).cmp(&(other.height.round() as isize)))
2981 }
2982}
2983
2984impl Hash for Size {
2986 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
2988 (self.width.round() as isize).hash(state);
2989 (self.height.round() as isize).hash(state);
2990 }
2991}
2992impl PartialEq for Size {
2993 fn eq(&self, other: &Self) -> bool {
2994 round_eq(self.width, other.width) && round_eq(self.height, other.height)
2995 }
2996}
2997impl Eq for Size {}
2998
2999impl Size {
3000 #[must_use] pub const fn zero() -> Self {
3001 Self::new(0.0, 0.0)
3002 }
3003 #[must_use] pub const fn new(width: f32, height: f32) -> Self {
3004 Self { width, height }
3005 }
3006}
3007
3008#[derive(Debug, Default, Clone, Copy, PartialOrd)]
3009pub struct Point {
3010 pub x: f32,
3011 pub y: f32,
3012}
3013
3014impl Hash for Point {
3016 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3018 (self.x.round() as isize).hash(state);
3019 (self.y.round() as isize).hash(state);
3020 }
3021}
3022
3023impl PartialEq for Point {
3024 fn eq(&self, other: &Self) -> bool {
3025 round_eq(self.x, other.x) && round_eq(self.y, other.y)
3026 }
3027}
3028
3029impl Eq for Point {}
3030
3031#[derive(Debug, Clone, PartialOrd)]
3032pub enum ShapeDefinition {
3033 Rectangle {
3034 size: Size,
3035 corner_radius: Option<f32>,
3036 },
3037 Circle {
3038 radius: f32,
3039 },
3040 Ellipse {
3041 radii: Size,
3042 },
3043 Polygon {
3044 points: Vec<Point>,
3045 },
3046 Path {
3047 segments: Vec<PathSegment>,
3048 },
3049}
3050
3051impl Hash for ShapeDefinition {
3053 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3055 discriminant(self).hash(state);
3056 match self {
3057 Self::Rectangle {
3058 size,
3059 corner_radius,
3060 } => {
3061 size.hash(state);
3062 corner_radius.map(|r| r.round() as isize).hash(state);
3063 }
3064 Self::Circle { radius } => {
3065 (radius.round() as isize).hash(state);
3066 }
3067 Self::Ellipse { radii } => {
3068 radii.hash(state);
3069 }
3070 Self::Polygon { points } => {
3071 points.hash(state);
3073 }
3074 Self::Path { segments } => {
3075 segments.hash(state);
3077 }
3078 }
3079 }
3080}
3081
3082impl PartialEq for ShapeDefinition {
3083 fn eq(&self, other: &Self) -> bool {
3084 match (self, other) {
3085 (
3086 Self::Rectangle {
3087 size: s1,
3088 corner_radius: r1,
3089 },
3090 Self::Rectangle {
3091 size: s2,
3092 corner_radius: r2,
3093 },
3094 ) => {
3095 s1 == s2
3096 && match (r1, r2) {
3097 (None, None) => true,
3098 (Some(v1), Some(v2)) => round_eq(*v1, *v2),
3099 _ => false,
3100 }
3101 }
3102 (Self::Circle { radius: r1 }, Self::Circle { radius: r2 }) => {
3103 round_eq(*r1, *r2)
3104 }
3105 (Self::Ellipse { radii: r1 }, Self::Ellipse { radii: r2 }) => {
3106 r1 == r2
3107 }
3108 (Self::Polygon { points: p1 }, Self::Polygon { points: p2 }) => {
3109 p1 == p2
3110 }
3111 (Self::Path { segments: s1 }, Self::Path { segments: s2 }) => {
3112 s1 == s2
3113 }
3114 _ => false,
3115 }
3116 }
3117}
3118impl Eq for ShapeDefinition {}
3119
3120impl ShapeDefinition {
3121 #[must_use] pub fn get_size(&self) -> Size {
3123 match self {
3124 Self::Rectangle { size, .. } => *size,
3126
3127 Self::Circle { radius } => {
3129 let diameter = radius * 2.0;
3130 Size::new(diameter, diameter)
3131 }
3132
3133 Self::Ellipse { radii } => Size::new(radii.width * 2.0, radii.height * 2.0),
3135
3136 Self::Polygon { points } => calculate_bounding_box_size(points),
3138
3139 Self::Path { segments } => {
3146 let mut points = Vec::new();
3147 let mut current_pos = Point { x: 0.0, y: 0.0 };
3148
3149 for segment in segments {
3150 match segment {
3151 PathSegment::MoveTo(p) | PathSegment::LineTo(p) => {
3152 points.push(*p);
3153 current_pos = *p;
3154 }
3155 PathSegment::QuadTo { control, end } => {
3156 points.push(current_pos);
3157 points.push(*control);
3158 points.push(*end);
3159 current_pos = *end;
3160 }
3161 PathSegment::CurveTo {
3162 control1,
3163 control2,
3164 end,
3165 } => {
3166 points.push(current_pos);
3167 points.push(*control1);
3168 points.push(*control2);
3169 points.push(*end);
3170 current_pos = *end;
3171 }
3172 PathSegment::Arc {
3173 center,
3174 radius,
3175 start_angle,
3176 end_angle,
3177 } => {
3178 let start_point = Point {
3180 x: center.x + radius * start_angle.cos(),
3181 y: center.y + radius * start_angle.sin(),
3182 };
3183 let end_point = Point {
3184 x: center.x + radius * end_angle.cos(),
3185 y: center.y + radius * end_angle.sin(),
3186 };
3187 points.push(start_point);
3188 points.push(end_point);
3189
3190 let mut normalized_end = *end_angle;
3194 #[allow(clippy::while_float)] while normalized_end < *start_angle {
3196 normalized_end += 2.0 * std::f32::consts::PI;
3197 }
3198
3199 let mut check_angle = (*start_angle / std::f32::consts::FRAC_PI_2)
3202 .ceil()
3203 * std::f32::consts::FRAC_PI_2;
3204
3205 #[allow(clippy::while_float)] while check_angle < normalized_end {
3210 points.push(Point {
3211 x: center.x + radius * check_angle.cos(),
3212 y: center.y + radius * check_angle.sin(),
3213 });
3214 check_angle += std::f32::consts::FRAC_PI_2;
3215 }
3216
3217 current_pos = end_point;
3220 }
3221 PathSegment::Close => {
3222 }
3224 }
3225 }
3226 calculate_bounding_box_size(&points)
3227 }
3228 }
3229 }
3230}
3231
3232pub(crate) fn resolve_effective_alignment(
3240 text_align: TextAlign,
3241 text_align_last: TextAlign,
3242 is_last_or_forced: bool,
3243) -> TextAlign {
3244 if is_last_or_forced {
3245 if text_align_last == TextAlign::default() {
3246 if text_align == TextAlign::Justify { TextAlign::Start } else { text_align }
3247 } else {
3248 text_align_last
3249 }
3250 } else {
3251 text_align
3252 }
3253}
3254
3255fn calculate_bounding_box_size(points: &[Point]) -> Size {
3257 if points.is_empty() {
3258 return Size::zero();
3259 }
3260
3261 let mut min_x = f32::MAX;
3262 let mut max_x = f32::MIN;
3263 let mut min_y = f32::MAX;
3264 let mut max_y = f32::MIN;
3265
3266 for point in points {
3267 min_x = min_x.min(point.x);
3268 max_x = max_x.max(point.x);
3269 min_y = min_y.min(point.y);
3270 max_y = max_y.max(point.y);
3271 }
3272
3273 if min_x > max_x || min_y > max_y {
3275 return Size::zero();
3276 }
3277
3278 Size::new(max_x - min_x, max_y - min_y)
3279}
3280
3281#[derive(Debug, Clone, PartialOrd)]
3282pub struct Stroke {
3283 pub color: ColorU,
3284 pub width: f32,
3285 pub dash_pattern: Option<Vec<f32>>,
3286}
3287
3288impl Hash for Stroke {
3290 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3292 self.color.hash(state);
3293 (self.width.round() as isize).hash(state);
3294
3295 match &self.dash_pattern {
3297 None => 0u8.hash(state), Some(pattern) => {
3299 1u8.hash(state); pattern.len().hash(state); for &val in pattern {
3302 (val.round() as isize).hash(state); }
3304 }
3305 }
3306 }
3307}
3308
3309impl PartialEq for Stroke {
3310 fn eq(&self, other: &Self) -> bool {
3311 if self.color != other.color || !round_eq(self.width, other.width) {
3312 return false;
3313 }
3314 match (&self.dash_pattern, &other.dash_pattern) {
3315 (None, None) => true,
3316 (Some(p1), Some(p2)) => {
3317 p1.len() == p2.len() && p1.iter().zip(p2.iter()).all(|(a, b)| round_eq(*a, *b))
3318 }
3319 _ => false,
3320 }
3321 }
3322}
3323
3324impl Eq for Stroke {}
3325
3326#[allow(clippy::cast_possible_truncation)] fn round_eq(a: f32, b: f32) -> bool {
3329 (a.round() as isize) == (b.round() as isize)
3330}
3331
3332#[derive(Debug, Clone)]
3333pub enum ShapeBoundary {
3334 Rectangle(Rect),
3335 Circle { center: Point, radius: f32 },
3336 Ellipse { center: Point, radii: Size },
3337 Polygon { points: Vec<Point> },
3338 Path { segments: Vec<PathSegment> },
3339}
3340
3341impl ShapeBoundary {
3342 #[allow(clippy::suboptimal_flops)] #[must_use] pub fn inflate(&self, margin: f32) -> Self {
3344 if margin == 0.0 {
3345 return self.clone();
3346 }
3347 match self {
3348 Self::Rectangle(rect) => Self::Rectangle(Rect {
3349 x: rect.x - margin,
3350 y: rect.y - margin,
3351 width: (rect.width + margin * 2.0).max(0.0),
3352 height: (rect.height + margin * 2.0).max(0.0),
3353 }),
3354 Self::Circle { center, radius } => Self::Circle {
3355 center: *center,
3356 radius: radius + margin,
3357 },
3358 _ => self.clone(),
3361 }
3362 }
3363}
3364
3365impl Hash for ShapeBoundary {
3367 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3369 discriminant(self).hash(state);
3370 match self {
3371 Self::Rectangle(rect) => rect.hash(state),
3372 Self::Circle { center, radius } => {
3373 center.hash(state);
3374 (radius.round() as isize).hash(state);
3375 }
3376 Self::Ellipse { center, radii } => {
3377 center.hash(state);
3378 radii.hash(state);
3379 }
3380 Self::Polygon { points } => points.hash(state),
3381 Self::Path { segments } => segments.hash(state),
3382 }
3383 }
3384}
3385impl PartialEq for ShapeBoundary {
3386 fn eq(&self, other: &Self) -> bool {
3387 match (self, other) {
3388 (Self::Rectangle(r1), Self::Rectangle(r2)) => r1 == r2,
3389 (
3390 Self::Circle {
3391 center: c1,
3392 radius: r1,
3393 },
3394 Self::Circle {
3395 center: c2,
3396 radius: r2,
3397 },
3398 ) => c1 == c2 && round_eq(*r1, *r2),
3399 (
3400 Self::Ellipse {
3401 center: c1,
3402 radii: r1,
3403 },
3404 Self::Ellipse {
3405 center: c2,
3406 radii: r2,
3407 },
3408 ) => c1 == c2 && r1 == r2,
3409 (Self::Polygon { points: p1 }, Self::Polygon { points: p2 }) => {
3410 p1 == p2
3411 }
3412 (Self::Path { segments: s1 }, Self::Path { segments: s2 }) => {
3413 s1 == s2
3414 }
3415 _ => false,
3416 }
3417 }
3418}
3419impl Eq for ShapeBoundary {}
3420
3421impl ShapeBoundary {
3422 #[allow(clippy::too_many_lines)] pub fn from_css_shape(
3432 css_shape: &azul_css::shape::CssShape,
3433 reference_box: Rect,
3434 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
3435 ) -> Self {
3436 use azul_css::shape::CssShape;
3437
3438 if let Some(msgs) = debug_messages {
3439 msgs.push(LayoutDebugMessage::info(format!(
3440 "[ShapeBoundary::from_css_shape] Input CSS shape: {css_shape:?}"
3441 )));
3442 msgs.push(LayoutDebugMessage::info(format!(
3443 "[ShapeBoundary::from_css_shape] Reference box: {reference_box:?}"
3444 )));
3445 }
3446
3447 let result = match css_shape {
3448 CssShape::Circle(circle) => {
3449 let center = Point {
3450 x: reference_box.x + circle.center.x,
3451 y: reference_box.y + circle.center.y,
3452 };
3453 if let Some(msgs) = debug_messages {
3454 msgs.push(LayoutDebugMessage::info(format!(
3455 "[ShapeBoundary::from_css_shape] Circle - CSS center: ({}, {}), radius: {}",
3456 circle.center.x, circle.center.y, circle.radius
3457 )));
3458 msgs.push(LayoutDebugMessage::info(format!(
3459 "[ShapeBoundary::from_css_shape] Circle - Absolute center: ({}, {}), \
3460 radius: {}",
3461 center.x, center.y, circle.radius
3462 )));
3463 }
3464 Self::Circle {
3465 center,
3466 radius: circle.radius,
3467 }
3468 }
3469
3470 CssShape::Ellipse(ellipse) => {
3471 let center = Point {
3472 x: reference_box.x + ellipse.center.x,
3473 y: reference_box.y + ellipse.center.y,
3474 };
3475 let radii = Size {
3476 width: ellipse.radius_x,
3477 height: ellipse.radius_y,
3478 };
3479 if let Some(msgs) = debug_messages {
3480 msgs.push(LayoutDebugMessage::info(format!(
3481 "[ShapeBoundary::from_css_shape] Ellipse - center: ({}, {}), radii: ({}, \
3482 {})",
3483 center.x, center.y, radii.width, radii.height
3484 )));
3485 }
3486 Self::Ellipse { center, radii }
3487 }
3488
3489 CssShape::Polygon(polygon) => {
3490 let points = polygon
3491 .points
3492 .as_ref()
3493 .iter()
3494 .map(|pt| Point {
3495 x: reference_box.x + pt.x,
3496 y: reference_box.y + pt.y,
3497 })
3498 .collect();
3499 if let Some(msgs) = debug_messages {
3500 msgs.push(LayoutDebugMessage::info(format!(
3501 "[ShapeBoundary::from_css_shape] Polygon - {} points",
3502 polygon.points.as_ref().len()
3503 )));
3504 }
3505 Self::Polygon { points }
3506 }
3507
3508 CssShape::Inset(inset) => {
3509 let x = reference_box.x + inset.inset_left;
3511 let y = reference_box.y + inset.inset_top;
3512 let width = reference_box.width - inset.inset_left - inset.inset_right;
3513 let height = reference_box.height - inset.inset_top - inset.inset_bottom;
3514
3515 if let Some(msgs) = debug_messages {
3516 msgs.push(LayoutDebugMessage::info(format!(
3517 "[ShapeBoundary::from_css_shape] Inset - insets: ({}, {}, {}, {})",
3518 inset.inset_top, inset.inset_right, inset.inset_bottom, inset.inset_left
3519 )));
3520 msgs.push(LayoutDebugMessage::info(format!(
3521 "[ShapeBoundary::from_css_shape] Inset - resulting rect: x={x}, y={y}, \
3522 w={width}, h={height}"
3523 )));
3524 }
3525
3526 Self::Rectangle(Rect {
3527 x,
3528 y,
3529 width: width.max(0.0),
3530 height: height.max(0.0),
3531 })
3532 }
3533
3534 CssShape::Path(path) => {
3535 let segments = azul_core::path_parser::parse_svg_path_d(path.data.as_str())
3541 .map_or_else(|_| Vec::new(), |multipolygon| {
3542 flatten_svg_to_path_segments(&multipolygon, reference_box)
3543 });
3544 if let Some(msgs) = debug_messages {
3545 msgs.push(LayoutDebugMessage::info(format!(
3546 "[ShapeBoundary::from_css_shape] Path - parsed {} flattened segments",
3547 segments.len()
3548 )));
3549 }
3550 if segments.is_empty() {
3551 Self::Rectangle(reference_box)
3554 } else {
3555 Self::Path { segments }
3556 }
3557 }
3558 };
3559
3560 if let Some(msgs) = debug_messages {
3561 msgs.push(LayoutDebugMessage::info(format!(
3562 "[ShapeBoundary::from_css_shape] Result: {result:?}"
3563 )));
3564 }
3565 result
3566 }
3567}
3568
3569#[derive(Copy, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3570pub struct InlineBreak {
3571 pub break_type: BreakType,
3572 pub clear: ClearType,
3573 pub content_index: usize,
3574}
3575
3576#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3578pub enum BreakType {
3579 Soft, Hard, Page, Column, }
3584
3585#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3586pub enum ClearType {
3587 None,
3588 Left,
3589 Right,
3590 Both,
3591}
3592
3593#[derive(Debug, Clone)]
3595pub(crate) struct ShapeConstraints {
3596 pub(crate) boundaries: Vec<ShapeBoundary>,
3597 pub(crate) exclusions: Vec<ShapeBoundary>,
3598 pub(crate) writing_mode: WritingMode,
3599 pub(crate) text_align: TextAlign,
3600 pub(crate) line_height: LineHeight,
3601}
3602
3603#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
3604pub enum WritingMode {
3605 #[default]
3606 HorizontalTb, VerticalRl, VerticalLr, SidewaysRl, SidewaysLr, }
3612
3613impl WritingMode {
3614 #[must_use] pub const fn is_advance_horizontal(&self) -> bool {
3616 matches!(
3617 self,
3618 Self::HorizontalTb | Self::SidewaysRl | Self::SidewaysLr
3619 )
3620 }
3621}
3622
3623#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
3624pub enum JustifyContent {
3625 #[default]
3626 None,
3627 InterWord, InterCharacter, Distribute, Kashida, }
3632
3633#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
3635pub enum TextAlign {
3636 #[default]
3637 Left,
3638 Right,
3639 Center,
3640 Justify,
3641 Start,
3642 End, JustifyAll, }
3645
3646#[derive(Debug, Clone, Copy, PartialEq, Default, Eq, PartialOrd, Ord, Hash)]
3649pub enum TextOrientation {
3650 #[default]
3651 Mixed, Upright, Sideways, }
3655
3656#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3657#[derive(Default)]
3658pub struct TextDecoration {
3659 pub underline: bool,
3660 pub strikethrough: bool,
3661 pub overline: bool,
3662}
3663
3664
3665impl TextDecoration {
3666 #[must_use] pub fn from_css(css: azul_css::props::style::text::StyleTextDecoration) -> Self {
3672 use azul_css::props::style::text::StyleTextDecoration;
3673 match css {
3674 StyleTextDecoration::None => Self::default(),
3675 StyleTextDecoration::Underline => Self {
3676 underline: true,
3677 strikethrough: false,
3678 overline: false,
3679 },
3680 StyleTextDecoration::Overline => Self {
3681 underline: false,
3682 strikethrough: false,
3683 overline: true,
3684 },
3685 StyleTextDecoration::LineThrough => Self {
3686 underline: false,
3687 strikethrough: true,
3688 overline: false,
3689 },
3690 }
3691 }
3692}
3693
3694#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
3695pub enum TextTransform {
3696 #[default]
3697 None,
3698 Uppercase,
3699 Lowercase,
3700 Capitalize,
3701 FullWidth,
3703}
3704
3705pub type FourCc = [u8; 4];
3707
3708#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
3710pub enum Spacing {
3711 Px(i32), PxF(f32),
3717 Em(f32),
3718}
3719
3720impl Eq for Spacing {}
3724
3725impl Hash for Spacing {
3726 fn hash<H: Hasher>(&self, state: &mut H) {
3727 discriminant(self).hash(state);
3729 match self {
3730 Self::Px(val) => val.hash(state),
3731 Self::PxF(val) | Self::Em(val) => val.to_bits().hash(state),
3734 }
3735 }
3736}
3737
3738impl Default for Spacing {
3739 fn default() -> Self {
3740 Self::Px(0)
3741 }
3742}
3743
3744impl Spacing {
3745 #[allow(clippy::cast_precision_loss)] #[must_use]
3748 pub fn resolve_px(self, font_size_px: f32) -> f32 {
3749 match self {
3750 Self::Px(px) => px as f32,
3751 Self::PxF(px) => px,
3752 Self::Em(em) => em * font_size_px,
3753 }
3754 }
3755}
3756
3757impl Default for FontHash {
3758 fn default() -> Self {
3759 Self::invalid()
3760 }
3761}
3762
3763#[derive(Debug, Clone, PartialEq)]
3765pub struct StyleProperties {
3766 pub font_stack: FontStack,
3770 pub font_size_px: f32,
3771 pub color: ColorU,
3772 pub background_color: Option<ColorU>,
3783 pub background_content: Vec<StyleBackgroundContent>,
3786 pub border: Option<InlineBorderInfo>,
3788 pub letter_spacing: Spacing,
3790 pub word_spacing: Spacing,
3791
3792 pub line_height: LineHeight,
3793 pub text_decoration: TextDecoration,
3794
3795 pub font_features: Vec<String>,
3797
3798 pub font_variations: Vec<(FourCc, f32)>,
3800 pub tab_size: f32,
3802 pub text_transform: TextTransform,
3804 pub writing_mode: WritingMode,
3806 pub text_orientation: TextOrientation,
3807 pub text_combine_upright: Option<TextCombineUpright>,
3809
3810 pub font_variant_caps: FontVariantCaps,
3812 pub font_variant_numeric: FontVariantNumeric,
3813 pub font_variant_ligatures: FontVariantLigatures,
3814 pub font_variant_east_asian: FontVariantEastAsian,
3815
3816 pub vertical_align: VerticalAlign,
3821}
3822
3823impl Default for StyleProperties {
3824 fn default() -> Self {
3825 const FONT_SIZE: f32 = 16.0;
3826 const TAB_SIZE: f32 = 8.0;
3827 Self {
3828 font_stack: FontStack::default(),
3829 font_size_px: FONT_SIZE,
3830 color: ColorU::default(),
3831 background_color: None,
3832 background_content: Vec::new(),
3833 border: None,
3834 letter_spacing: Spacing::default(), word_spacing: Spacing::default(), line_height: LineHeight::Normal,
3837 text_decoration: TextDecoration::default(),
3838 font_features: Vec::new(),
3839 font_variations: Vec::new(),
3840 tab_size: TAB_SIZE, text_transform: TextTransform::default(),
3842 writing_mode: WritingMode::default(),
3843 text_orientation: TextOrientation::default(),
3844 text_combine_upright: None,
3845 font_variant_caps: FontVariantCaps::default(),
3846 font_variant_numeric: FontVariantNumeric::default(),
3847 font_variant_ligatures: FontVariantLigatures::default(),
3848 font_variant_east_asian: FontVariantEastAsian::default(),
3849 vertical_align: VerticalAlign::Baseline,
3850 }
3851 }
3852}
3853
3854impl Hash for StyleProperties {
3855 #[allow(clippy::cast_possible_truncation)] fn hash<H: Hasher>(&self, state: &mut H) {
3857 self.font_stack.hash(state);
3858 self.color.hash(state);
3859 self.background_color.hash(state);
3860 self.text_decoration.hash(state);
3861 self.font_features.hash(state);
3862 self.writing_mode.hash(state);
3863 self.text_orientation.hash(state);
3864 self.text_combine_upright.hash(state);
3865 self.vertical_align.hash(state);
3866 self.letter_spacing.hash(state);
3867 self.word_spacing.hash(state);
3868
3869 (self.font_size_px.round() as isize).hash(state);
3871 self.line_height.hash(state);
3872 }
3873}
3874
3875impl StyleProperties {
3876 #[allow(clippy::cast_possible_truncation)] #[must_use] pub fn layout_hash(&self) -> u64 {
3896 use std::hash::Hasher;
3897 let mut hasher = DefaultHasher::new();
3898
3899 self.font_stack.hash(&mut hasher);
3901 self.font_size_px.to_bits().hash(&mut hasher);
3905 self.font_features.hash(&mut hasher);
3906 for (tag, value) in &self.font_variations {
3908 tag.hash(&mut hasher);
3909 (value.round() as i32).hash(&mut hasher);
3910 }
3911
3912 self.letter_spacing.hash(&mut hasher);
3914 self.word_spacing.hash(&mut hasher);
3915 self.line_height.hash(&mut hasher);
3916 (self.tab_size.round() as isize).hash(&mut hasher);
3917
3918 self.writing_mode.hash(&mut hasher);
3920 self.text_orientation.hash(&mut hasher);
3921 self.text_combine_upright.hash(&mut hasher);
3922
3923 self.text_transform.hash(&mut hasher);
3925
3926 self.font_variant_caps.hash(&mut hasher);
3928 self.font_variant_numeric.hash(&mut hasher);
3929 self.font_variant_ligatures.hash(&mut hasher);
3930 self.font_variant_east_asian.hash(&mut hasher);
3931
3932 hasher.finish()
3933 }
3934
3935 #[must_use] pub fn layout_eq(&self, other: &Self) -> bool {
3944 self.layout_hash() == other.layout_hash()
3945 }
3946}
3947
3948#[derive(Copy, Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
3949pub enum TextCombineUpright {
3950 None,
3951 All, Digits(u8), }
3954
3955#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3956pub enum GlyphSource {
3957 Char,
3959 Hyphen,
3961}
3962
3963#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3964pub enum CharacterClass {
3965 Space, Punctuation, Letter, Ideograph, Symbol, Combining, }
3972
3973#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3974pub enum GlyphOrientation {
3975 Horizontal, Vertical, Upright, Mixed, }
3980
3981#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3983pub enum BidiDirection {
3984 Ltr,
3985 Rtl,
3986}
3987
3988impl BidiDirection {
3989 #[must_use] pub const fn is_rtl(&self) -> bool {
3990 matches!(self, Self::Rtl)
3991 }
3992}
3993
3994#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4000#[derive(Default)]
4001pub enum UnicodeBidi {
4002 #[default]
4003 Normal,
4004 Embed,
4005 Isolate,
4006 BidiOverride,
4007 IsolateOverride,
4008 Plaintext,
4009}
4010
4011
4012#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
4013pub enum FontVariantCaps {
4014 #[default]
4015 Normal,
4016 SmallCaps,
4017 AllSmallCaps,
4018 PetiteCaps,
4019 AllPetiteCaps,
4020 Unicase,
4021 TitlingCaps,
4022}
4023
4024#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
4025pub enum FontVariantNumeric {
4026 #[default]
4027 Normal,
4028 LiningNums,
4029 OldstyleNums,
4030 ProportionalNums,
4031 TabularNums,
4032 DiagonalFractions,
4033 StackedFractions,
4034 Ordinal,
4035 SlashedZero,
4036}
4037
4038#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
4039pub enum FontVariantLigatures {
4040 #[default]
4041 Normal,
4042 None,
4043 Common,
4044 NoCommon,
4045 Discretionary,
4046 NoDiscretionary,
4047 Historical,
4048 NoHistorical,
4049 Contextual,
4050 NoContextual,
4051}
4052
4053#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
4054pub enum FontVariantEastAsian {
4055 #[default]
4056 Normal,
4057 Jis78,
4058 Jis83,
4059 Jis90,
4060 Jis04,
4061 Simplified,
4062 Traditional,
4063 FullWidth,
4064 ProportionalWidth,
4065 Ruby,
4066}
4067
4068#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4069pub struct BidiLevel(u8);
4070
4071impl BidiLevel {
4072 #[must_use] pub const fn new(level: u8) -> Self {
4073 Self(level)
4074 }
4075 #[must_use] pub const fn is_rtl(&self) -> bool {
4076 self.0 % 2 == 1
4077 }
4078 #[must_use] pub const fn level(&self) -> u8 {
4079 self.0
4080 }
4081}
4082
4083#[derive(Debug, Clone)]
4085pub struct StyleOverride {
4086 pub target: ContentIndex,
4088 pub style: PartialStyleProperties,
4091}
4092
4093#[derive(Debug, Clone, Default)]
4094pub struct PartialStyleProperties {
4095 pub font_stack: Option<FontStack>,
4096 pub font_size_px: Option<f32>,
4097 pub color: Option<ColorU>,
4098 pub letter_spacing: Option<Spacing>,
4099 pub word_spacing: Option<Spacing>,
4100 pub line_height: Option<LineHeight>,
4101 pub text_decoration: Option<TextDecoration>,
4102 pub font_features: Option<Vec<String>>,
4103 pub font_variations: Option<Vec<(FourCc, f32)>>,
4104 pub tab_size: Option<f32>,
4105 pub text_transform: Option<TextTransform>,
4106 pub writing_mode: Option<WritingMode>,
4107 pub text_orientation: Option<TextOrientation>,
4108 pub text_combine_upright: Option<Option<TextCombineUpright>>,
4109 pub font_variant_caps: Option<FontVariantCaps>,
4110 pub font_variant_numeric: Option<FontVariantNumeric>,
4111 pub font_variant_ligatures: Option<FontVariantLigatures>,
4112 pub font_variant_east_asian: Option<FontVariantEastAsian>,
4113}
4114
4115impl Hash for PartialStyleProperties {
4116 fn hash<H: Hasher>(&self, state: &mut H) {
4117 self.font_stack.hash(state);
4118 self.font_size_px.map(f32::to_bits).hash(state);
4119 self.color.hash(state);
4120 self.letter_spacing.hash(state);
4121 self.word_spacing.hash(state);
4122 self.line_height.hash(state);
4123 self.text_decoration.hash(state);
4124 self.font_features.hash(state);
4125
4126 if let Some(v) = self.font_variations.as_ref() {
4128 for (tag, val) in v {
4129 tag.hash(state);
4130 val.to_bits().hash(state);
4131 }
4132 }
4133
4134 self.tab_size.map(f32::to_bits).hash(state);
4135 self.text_transform.hash(state);
4136 self.writing_mode.hash(state);
4137 self.text_orientation.hash(state);
4138 self.text_combine_upright.hash(state);
4139 self.font_variant_caps.hash(state);
4140 self.font_variant_numeric.hash(state);
4141 self.font_variant_ligatures.hash(state);
4142 self.font_variant_east_asian.hash(state);
4143 }
4144}
4145
4146impl PartialEq for PartialStyleProperties {
4147 fn eq(&self, other: &Self) -> bool {
4148 self.font_stack == other.font_stack &&
4149 self.font_size_px.map(f32::to_bits) == other.font_size_px.map(f32::to_bits) &&
4150 self.color == other.color &&
4151 self.letter_spacing == other.letter_spacing &&
4152 self.word_spacing == other.word_spacing &&
4153 self.line_height == other.line_height &&
4154 self.text_decoration == other.text_decoration &&
4155 self.font_features == other.font_features &&
4156 self.font_variations == other.font_variations && self.tab_size.map(f32::to_bits) == other.tab_size.map(f32::to_bits) &&
4158 self.text_transform == other.text_transform &&
4159 self.writing_mode == other.writing_mode &&
4160 self.text_orientation == other.text_orientation &&
4161 self.text_combine_upright == other.text_combine_upright &&
4162 self.font_variant_caps == other.font_variant_caps &&
4163 self.font_variant_numeric == other.font_variant_numeric &&
4164 self.font_variant_ligatures == other.font_variant_ligatures &&
4165 self.font_variant_east_asian == other.font_variant_east_asian
4166 }
4167}
4168
4169impl Eq for PartialStyleProperties {}
4170
4171impl StyleProperties {
4172 fn apply_override(&self, partial: &PartialStyleProperties) -> Self {
4173 let mut new_style = self.clone();
4174 if let Some(val) = &partial.font_stack {
4175 new_style.font_stack = val.clone();
4176 }
4177 if let Some(val) = partial.font_size_px {
4178 new_style.font_size_px = val;
4179 }
4180 if let Some(val) = &partial.color {
4181 new_style.color = *val;
4182 }
4183 if let Some(val) = partial.letter_spacing {
4184 new_style.letter_spacing = val;
4185 }
4186 if let Some(val) = partial.word_spacing {
4187 new_style.word_spacing = val;
4188 }
4189 if let Some(val) = partial.line_height {
4190 new_style.line_height = val;
4191 }
4192 if let Some(val) = &partial.text_decoration {
4193 new_style.text_decoration = *val;
4194 }
4195 if let Some(val) = &partial.font_features {
4196 new_style.font_features.clone_from(val);
4197 }
4198 if let Some(val) = &partial.font_variations {
4199 new_style.font_variations.clone_from(val);
4200 }
4201 if let Some(val) = partial.tab_size {
4202 new_style.tab_size = val;
4203 }
4204 if let Some(val) = partial.text_transform {
4205 new_style.text_transform = val;
4206 }
4207 if let Some(val) = partial.writing_mode {
4208 new_style.writing_mode = val;
4209 }
4210 if let Some(val) = partial.text_orientation {
4211 new_style.text_orientation = val;
4212 }
4213 if let Some(val) = &partial.text_combine_upright {
4214 new_style.text_combine_upright.clone_from(val);
4215 }
4216 if let Some(val) = partial.font_variant_caps {
4217 new_style.font_variant_caps = val;
4218 }
4219 if let Some(val) = partial.font_variant_numeric {
4220 new_style.font_variant_numeric = val;
4221 }
4222 if let Some(val) = partial.font_variant_ligatures {
4223 new_style.font_variant_ligatures = val;
4224 }
4225 if let Some(val) = partial.font_variant_east_asian {
4226 new_style.font_variant_east_asian = val;
4227 }
4228 new_style
4229 }
4230}
4231
4232#[derive(Debug, Clone, Copy, PartialEq)]
4234pub enum GlyphKind {
4235 Character,
4237 Hyphen,
4239 NotDef,
4241 Kashida {
4243 width: f32,
4245 },
4246}
4247
4248#[derive(Debug, Clone)]
4255#[repr(C, u8)]
4256pub enum LogicalItem {
4257 Text {
4258 source: ContentIndex,
4260 text: String,
4262 style: Arc<StyleProperties>,
4263 marker_position_outside: Option<bool>,
4267 source_node_id: Option<NodeId>,
4270 },
4271 CombinedText {
4274 source: ContentIndex,
4275 text: String,
4276 style: Arc<StyleProperties>,
4277 },
4278 Ruby {
4279 source: ContentIndex,
4280 base_text: String,
4283 ruby_text: String,
4284 style: Arc<StyleProperties>,
4285 },
4286 Object {
4287 source: ContentIndex,
4289 content: InlineContent,
4291 },
4292 Tab {
4293 source: ContentIndex,
4294 style: Arc<StyleProperties>,
4295 },
4296 Break {
4297 source: ContentIndex,
4298 break_info: InlineBreak,
4299 },
4300}
4301
4302impl Hash for LogicalItem {
4303 fn hash<H: Hasher>(&self, state: &mut H) {
4304 discriminant(self).hash(state);
4305 match self {
4306 Self::Text {
4307 source,
4308 text,
4309 style,
4310 marker_position_outside,
4311 source_node_id,
4312 } => {
4313 source.hash(state);
4314 text.hash(state);
4315 style.as_ref().hash(state); marker_position_outside.hash(state);
4317 source_node_id.hash(state);
4318 }
4319 Self::CombinedText {
4320 source,
4321 text,
4322 style,
4323 } => {
4324 source.hash(state);
4325 text.hash(state);
4326 style.as_ref().hash(state);
4327 }
4328 Self::Ruby {
4329 source,
4330 base_text,
4331 ruby_text,
4332 style,
4333 } => {
4334 source.hash(state);
4335 base_text.hash(state);
4336 ruby_text.hash(state);
4337 style.as_ref().hash(state);
4338 }
4339 Self::Object { source, content } => {
4340 source.hash(state);
4341 content.hash(state);
4342 }
4343 Self::Tab { source, style } => {
4344 source.hash(state);
4345 style.as_ref().hash(state);
4346 }
4347 Self::Break { source, break_info } => {
4348 source.hash(state);
4349 break_info.hash(state);
4350 }
4351 }
4352 }
4353}
4354
4355#[derive(Debug, Clone)]
4358pub struct VisualItem {
4359 pub logical_source: LogicalItem,
4362 pub bidi_level: BidiLevel,
4364 pub script: Script,
4366 pub text: String,
4368 pub run_byte_offset: usize,
4374}
4375
4376#[derive(Debug, Clone)]
4383#[repr(C, u8)]
4384pub enum ShapedItem {
4385 Cluster(ShapedCluster),
4386 CombinedBlock {
4389 source: ContentIndex,
4390 glyphs: ShapedGlyphVec,
4392 bounds: Rect,
4393 baseline_offset: f32,
4394 },
4395 Object {
4396 source: ContentIndex,
4397 bounds: Rect,
4398 baseline_offset: f32,
4399 content: InlineContent,
4401 },
4402 Tab {
4403 source: ContentIndex,
4404 bounds: Rect,
4405 },
4406 Break {
4407 source: ContentIndex,
4408 break_info: InlineBreak,
4409 },
4410}
4411
4412impl ShapedItem {
4413 #[must_use] pub const fn as_cluster(&self) -> Option<&ShapedCluster> {
4414 match self {
4415 Self::Cluster(c) => Some(c),
4416 _ => None,
4417 }
4418 }
4419 #[allow(clippy::match_same_arms)] #[must_use] pub fn bounds(&self) -> Rect {
4426 match self {
4427 Self::Cluster(cluster) => {
4428 let width = cluster.advance;
4430
4431 let (ascent, descent) = get_item_vertical_metrics_approx(self);
4435 let height = ascent + descent;
4436
4437 Rect {
4438 x: 0.0,
4439 y: 0.0,
4440 width,
4441 height,
4442 }
4443 }
4444 Self::CombinedBlock { bounds, .. } => *bounds,
4447 Self::Object { bounds, .. } => *bounds,
4448 Self::Tab { bounds, .. } => *bounds,
4449
4450 Self::Break { .. } => Rect::default(), }
4453 }
4454}
4455
4456#[derive(Debug, Clone)]
4458pub struct ShapedCluster {
4459 pub text: String,
4462 pub source_cluster_id: GraphemeClusterId,
4464 pub source_content_index: ContentIndex,
4466 pub source_node_id: Option<NodeId>,
4469 pub glyphs: ShapedGlyphVec,
4473 pub advance: f32,
4475 pub direction: BidiDirection,
4477 pub style: Arc<StyleProperties>,
4479 pub marker_position_outside: Option<bool>,
4483 pub is_first_fragment: bool,
4488 pub is_last_fragment: bool,
4491}
4492
4493#[derive(Debug, Clone)]
4495pub struct ShapedGlyph {
4496 pub kind: GlyphKind,
4498 pub glyph_id: u16,
4500 pub cluster_offset: u32,
4502 pub advance: f32,
4505 pub kerning: f32,
4508 pub offset: Point,
4510 pub vertical_advance: f32,
4512 pub vertical_offset: Point,
4514 pub script: Script,
4515 pub style: Arc<StyleProperties>,
4516 pub font_hash: u64,
4518 pub font_metrics: LayoutFontMetrics,
4520}
4521
4522impl ShapedGlyph {
4523 #[must_use] pub fn into_glyph_instance<T: ParsedFontTrait>(
4524 &self,
4525 writing_mode: WritingMode,
4526 loaded_fonts: &LoadedFonts<T>,
4527 ) -> GlyphInstance {
4528 let size = loaded_fonts
4529 .get_by_hash(self.font_hash)
4530 .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
4531 .unwrap_or_default();
4532
4533 let position = if writing_mode.is_advance_horizontal() {
4534 LogicalPosition {
4535 x: self.offset.x,
4536 y: self.offset.y,
4537 }
4538 } else {
4539 LogicalPosition {
4540 x: self.vertical_offset.x,
4541 y: self.vertical_offset.y,
4542 }
4543 };
4544
4545 GlyphInstance {
4546 index: u32::from(self.glyph_id),
4547 point: position,
4548 size,
4549 }
4550 }
4551
4552 #[must_use] pub fn into_glyph_instance_at<T: ParsedFontTrait>(
4555 &self,
4556 writing_mode: WritingMode,
4557 absolute_position: LogicalPosition,
4558 loaded_fonts: &LoadedFonts<T>,
4559 ) -> GlyphInstance {
4560 let size = loaded_fonts
4561 .get_by_hash(self.font_hash)
4562 .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
4563 .unwrap_or_default();
4564
4565 GlyphInstance {
4566 index: u32::from(self.glyph_id),
4567 point: absolute_position,
4568 size,
4569 }
4570 }
4571
4572 #[must_use] pub fn into_glyph_instance_at_simple(
4576 &self,
4577 _writing_mode: WritingMode,
4578 absolute_position: LogicalPosition,
4579 ) -> GlyphInstance {
4580 GlyphInstance {
4583 index: u32::from(self.glyph_id),
4584 point: absolute_position,
4585 size: LogicalSize::default(),
4586 }
4587 }
4588}
4589
4590#[derive(Debug, Clone)]
4593pub struct PositionedItem {
4594 pub item: ShapedItem,
4595 pub position: Point,
4596 pub line_index: usize,
4597}
4598
4599#[derive(Debug, Clone)]
4600pub struct UnifiedLayout {
4601 pub items: Vec<PositionedItem>,
4602 pub overflow: OverflowInfo,
4604}
4605
4606impl UnifiedLayout {
4607 #[must_use] pub fn bounds(&self) -> Rect {
4610 if self.items.is_empty() {
4611 return Rect::default();
4612 }
4613
4614 let mut min_x = f32::MAX;
4615 let mut min_y = f32::MAX;
4616 let mut max_x = f32::MIN;
4617 let mut max_y = f32::MIN;
4618
4619 for item in &self.items {
4620 let item_x = item.position.x;
4621 let item_y = item.position.y;
4622
4623 let item_bounds = item.item.bounds();
4625 let item_width = item_bounds.width;
4626 let item_height = item_bounds.height;
4627
4628 min_x = min_x.min(item_x);
4629 min_y = min_y.min(item_y);
4630 max_x = max_x.max(item_x + item_width);
4631 max_y = max_y.max(item_y + item_height);
4632 }
4633
4634 Rect {
4635 x: min_x,
4636 y: min_y,
4637 width: max_x - min_x,
4638 height: max_y - min_y,
4639 }
4640 }
4641
4642 #[must_use] pub const fn is_empty(&self) -> bool {
4643 self.items.is_empty()
4644 }
4645 #[must_use] pub fn first_baseline(&self) -> Option<f32> {
4646 self.items
4647 .iter()
4648 .find_map(|item| get_baseline_for_item(&item.item))
4649 }
4650
4651 #[must_use] pub fn last_baseline(&self) -> Option<f32> {
4652 self.items
4653 .iter()
4654 .rev()
4655 .find_map(|item| get_baseline_for_item(&item.item))
4656 }
4657
4658 #[allow(clippy::suboptimal_flops)] #[must_use] pub fn hittest_cursor(&self, point: LogicalPosition) -> Option<TextCursor> {
4665 if self.items.is_empty() {
4666 return None;
4667 }
4668
4669 let mut closest_item_idx = 0;
4671 let mut closest_distance = f32::MAX;
4672
4673 for (idx, item) in self.items.iter().enumerate() {
4674 if !matches!(item.item, ShapedItem::Cluster(_)) {
4676 continue;
4677 }
4678
4679 let item_bounds = item.item.bounds();
4680 let item_center_y = item.position.y + item_bounds.height / 2.0;
4681
4682 let vertical_distance = (point.y - item_center_y).abs();
4684
4685 let horizontal_distance = if point.x < item.position.x {
4687 item.position.x - point.x
4688 } else if point.x > item.position.x + item_bounds.width {
4689 point.x - (item.position.x + item_bounds.width)
4690 } else {
4691 0.0 };
4693
4694 let distance = vertical_distance * 2.0 + horizontal_distance;
4696
4697 if distance < closest_distance {
4698 closest_distance = distance;
4699 closest_item_idx = idx;
4700 }
4701 }
4702
4703 let closest_item = &self.items[closest_item_idx];
4705 let cluster = match &closest_item.item {
4706 ShapedItem::Cluster(c) => c,
4707 ShapedItem::Object { source, .. } | ShapedItem::CombinedBlock { source, .. } => {
4709 return Some(TextCursor {
4710 cluster_id: GraphemeClusterId {
4711 source_run: source.run_index,
4712 start_byte_in_run: source.item_index,
4713 },
4714 affinity: if point.x
4715 < closest_item.position.x + (closest_item.item.bounds().width / 2.0)
4716 {
4717 CursorAffinity::Leading
4718 } else {
4719 CursorAffinity::Trailing
4720 },
4721 });
4722 }
4723 _ => return None,
4724 };
4725
4726 let cluster_mid_x = closest_item.position.x + cluster.advance / 2.0;
4728 let affinity = if point.x < cluster_mid_x {
4729 CursorAffinity::Leading
4730 } else {
4731 CursorAffinity::Trailing
4732 };
4733
4734 Some(TextCursor {
4735 cluster_id: cluster.source_cluster_id,
4736 affinity,
4737 })
4738 }
4739
4740 #[allow(clippy::too_many_lines)] #[must_use] pub fn get_selection_rects(&self, range: &SelectionRange) -> Vec<LogicalRect> {
4744 let mut cluster_map: HashMap<GraphemeClusterId, &PositionedItem> = HashMap::new();
4746 for item in &self.items {
4747 if let Some(cluster) = item.item.as_cluster() {
4748 cluster_map.insert(cluster.source_cluster_id, item);
4749 }
4750 }
4751
4752 let (start_cursor, end_cursor) = if range.start.cluster_id > range.end.cluster_id
4754 || (range.start.cluster_id == range.end.cluster_id
4755 && range.start.affinity > range.end.affinity)
4756 {
4757 (range.end, range.start)
4758 } else {
4759 (range.start, range.end)
4760 };
4761
4762 let Some(start_item) = cluster_map.get(&start_cursor.cluster_id) else {
4764 return Vec::new();
4765 };
4766 let Some(end_item) = cluster_map.get(&end_cursor.cluster_id) else {
4767 return Vec::new();
4768 };
4769
4770 let mut rects = Vec::new();
4771
4772 let get_cursor_x = |item: &PositionedItem, affinity: CursorAffinity| -> f32 {
4776 let left = item.position.x;
4777 let right = item.position.x + get_item_measure(&item.item, false);
4778 let rtl = item.item.as_cluster().is_some_and(|c| c.direction.is_rtl());
4779 match (affinity, rtl) {
4780 (CursorAffinity::Leading, false) | (CursorAffinity::Trailing, true) => left,
4781 (CursorAffinity::Trailing, false) | (CursorAffinity::Leading, true) => right,
4782 }
4783 };
4784
4785 let get_line_bounds = |line_index: usize| -> Option<LogicalRect> {
4787 let items_on_line = self.items.iter().filter(|i| i.line_index == line_index);
4788
4789 let mut min_x: Option<f32> = None;
4790 let mut max_x: Option<f32> = None;
4791 let mut min_y: Option<f32> = None;
4792 let mut max_y: Option<f32> = None;
4793
4794 for item in items_on_line {
4795 let item_bounds = item.item.bounds();
4797 if item_bounds.width <= 0.0 && item_bounds.height <= 0.0 {
4798 continue;
4799 }
4800
4801 let item_x_end = item.position.x + item_bounds.width;
4802 let item_y_end = item.position.y + item_bounds.height;
4803
4804 min_x = Some(min_x.map_or(item.position.x, |mx| mx.min(item.position.x)));
4805 max_x = Some(max_x.map_or(item_x_end, |mx| mx.max(item_x_end)));
4806 min_y = Some(min_y.map_or(item.position.y, |my| my.min(item.position.y)));
4807 max_y = Some(max_y.map_or(item_y_end, |my| my.max(item_y_end)));
4808 }
4809
4810 if let (Some(min_x), Some(max_x), Some(min_y), Some(max_y)) =
4811 (min_x, max_x, min_y, max_y)
4812 {
4813 Some(LogicalRect {
4814 origin: LogicalPosition { x: min_x, y: min_y },
4815 size: LogicalSize {
4816 width: max_x - min_x,
4817 height: max_y - min_y,
4818 },
4819 })
4820 } else {
4821 None
4822 }
4823 };
4824
4825 if start_item.line_index == end_item.line_index {
4827 if let Some(line_bounds) = get_line_bounds(start_item.line_index) {
4828 let mut segments: Vec<(f32, f32, BidiDirection)> = Vec::new();
4835 for item in &self.items {
4836 if item.line_index != start_item.line_index {
4837 continue;
4838 }
4839 let Some(c) = item.item.as_cluster() else {
4840 continue;
4841 };
4842 let id = c.source_cluster_id;
4843 let after_start = id > start_cursor.cluster_id
4848 || (id == start_cursor.cluster_id
4849 && start_cursor.affinity == CursorAffinity::Leading);
4850 let before_end = id < end_cursor.cluster_id
4851 || (id == end_cursor.cluster_id
4852 && end_cursor.affinity == CursorAffinity::Trailing);
4853 if !(after_start && before_end) {
4854 continue;
4855 }
4856 let x0 = item.position.x;
4857 let x1 = item.position.x + get_item_measure(&item.item, false);
4858 let (lo, hi) = (x0.min(x1), x0.max(x1));
4859 if let Some(last) = segments.last_mut() {
4860 let contiguous = lo <= last.1 + 0.5 && hi >= last.0 - 0.5;
4861 if last.2 == c.direction && contiguous {
4862 last.0 = last.0.min(lo);
4863 last.1 = last.1.max(hi);
4864 continue;
4865 }
4866 }
4867 segments.push((lo, hi, c.direction));
4868 }
4869
4870 if segments.is_empty() {
4871 let start_x = get_cursor_x(start_item, start_cursor.affinity);
4874 let end_x = get_cursor_x(end_item, end_cursor.affinity);
4875 rects.push(LogicalRect {
4876 origin: LogicalPosition {
4877 x: start_x.min(end_x),
4878 y: line_bounds.origin.y,
4879 },
4880 size: LogicalSize {
4881 width: (end_x - start_x).abs(),
4882 height: line_bounds.size.height,
4883 },
4884 });
4885 } else {
4886 for (lo, hi, _dir) in segments {
4887 rects.push(LogicalRect {
4888 origin: LogicalPosition {
4889 x: lo,
4890 y: line_bounds.origin.y,
4891 },
4892 size: LogicalSize {
4893 width: hi - lo,
4894 height: line_bounds.size.height,
4895 },
4896 });
4897 }
4898 }
4899 }
4900 }
4901 else {
4903 if let Some(start_line_bounds) = get_line_bounds(start_item.line_index) {
4907 let start_x = get_cursor_x(start_item, start_cursor.affinity);
4908 let line_left = start_line_bounds.origin.x;
4909 let line_right = start_line_bounds.origin.x + start_line_bounds.size.width;
4910 let rtl = start_item.item.as_cluster().is_some_and(|c| c.direction.is_rtl());
4911 let (lo, hi) = if rtl { (line_left, start_x) } else { (start_x, line_right) };
4912 rects.push(LogicalRect {
4913 origin: LogicalPosition {
4914 x: lo,
4915 y: start_line_bounds.origin.y,
4916 },
4917 size: LogicalSize {
4918 width: hi - lo,
4919 height: start_line_bounds.size.height,
4920 },
4921 });
4922 }
4923
4924 for line_idx in (start_item.line_index + 1)..end_item.line_index {
4926 if let Some(line_bounds) = get_line_bounds(line_idx) {
4927 rects.push(line_bounds);
4928 }
4929 }
4930
4931 if let Some(end_line_bounds) = get_line_bounds(end_item.line_index) {
4935 let end_x = get_cursor_x(end_item, end_cursor.affinity);
4936 let line_left = end_line_bounds.origin.x;
4937 let line_right = end_line_bounds.origin.x + end_line_bounds.size.width;
4938 let rtl = end_item.item.as_cluster().is_some_and(|c| c.direction.is_rtl());
4939 let (lo, hi) = if rtl { (end_x, line_right) } else { (line_left, end_x) };
4940 rects.push(LogicalRect {
4941 origin: LogicalPosition {
4942 x: lo,
4943 y: end_line_bounds.origin.y,
4944 },
4945 size: LogicalSize {
4946 width: hi - lo,
4947 height: end_line_bounds.size.height,
4948 },
4949 });
4950 }
4951 }
4952
4953 rects
4954 }
4955
4956 #[must_use] pub fn get_cursor_rect(&self, cursor: &TextCursor) -> Option<LogicalRect> {
4958 let mut last_cluster: Option<(&PositionedItem, &ShapedCluster)> = None;
4960 for item in &self.items {
4961 if let ShapedItem::Cluster(cluster) = &item.item {
4962 if cluster.source_cluster_id == cursor.cluster_id {
4963 let line_height = item.item.bounds().height;
4965 let (lead_x, trail_x) = if cluster.direction.is_rtl() {
4969 (item.position.x + cluster.advance, item.position.x)
4970 } else {
4971 (item.position.x, item.position.x + cluster.advance)
4972 };
4973 let cursor_x = match cursor.affinity {
4974 CursorAffinity::Leading => lead_x,
4975 CursorAffinity::Trailing => trail_x,
4976 };
4977 return Some(LogicalRect {
4978 origin: LogicalPosition {
4979 x: cursor_x,
4980 y: item.position.y,
4981 },
4982 size: LogicalSize {
4983 width: 1.0,
4984 height: line_height,
4985 },
4986 });
4987 }
4988 last_cluster = Some((item, cluster));
4989 }
4990 }
4991 if let Some((item, cluster)) = last_cluster {
4993 if cursor.cluster_id.source_run == cluster.source_cluster_id.source_run
4994 && cursor.cluster_id.start_byte_in_run >= cluster.source_cluster_id.start_byte_in_run
4995 {
4996 let line_height = item.item.bounds().height;
4997 let past_end_x = if cluster.direction.is_rtl() {
5000 item.position.x
5001 } else {
5002 item.position.x + cluster.advance
5003 };
5004 return Some(LogicalRect {
5005 origin: LogicalPosition {
5006 x: past_end_x,
5007 y: item.position.y,
5008 },
5009 size: LogicalSize {
5010 width: 1.0,
5011 height: line_height,
5012 },
5013 });
5014 }
5015 }
5016 None
5017 }
5018
5019 #[must_use] pub fn get_first_cluster_cursor(&self) -> Option<TextCursor> {
5021 for item in &self.items {
5022 if let ShapedItem::Cluster(cluster) = &item.item {
5023 return Some(TextCursor {
5024 cluster_id: cluster.source_cluster_id,
5025 affinity: CursorAffinity::Leading,
5026 });
5027 }
5028 }
5029 None
5030 }
5031
5032 #[must_use] pub fn get_last_cluster_cursor(&self) -> Option<TextCursor> {
5034 for item in self.items.iter().rev() {
5035 if let ShapedItem::Cluster(cluster) = &item.item {
5036 return Some(TextCursor {
5037 cluster_id: cluster.source_cluster_id,
5038 affinity: CursorAffinity::Trailing,
5039 });
5040 }
5041 }
5042 None
5043 }
5044
5045 fn grapheme_stops(&self) -> Vec<GraphemeClusterId> {
5051 let mut stops: Vec<(GraphemeClusterId, &str)> = self
5052 .items
5053 .iter()
5054 .filter_map(|it| {
5055 it.item
5056 .as_cluster()
5057 .map(|c| (c.source_cluster_id, c.text.as_str()))
5058 })
5059 .collect();
5060 stops.sort_by(|a, b| {
5061 (a.0.source_run, a.0.start_byte_in_run).cmp(&(b.0.source_run, b.0.start_byte_in_run))
5062 });
5063 stops.dedup_by_key(|(id, _)| *id);
5064 stops
5065 .into_iter()
5066 .filter(|(_, text)| !Self::cluster_is_grapheme_continuation(text))
5067 .map(|(id, _)| id)
5068 .collect()
5069 }
5070
5071 fn cluster_is_grapheme_continuation(text: &str) -> bool {
5075 let Some(first) = text.chars().next() else {
5076 return false;
5077 };
5078 let mut probe = String::with_capacity(1 + first.len_utf8());
5081 probe.push('x');
5082 probe.push(first);
5083 probe.graphemes(true).count() == 1
5084 }
5085
5086 fn grapheme_caret_offset(stops: &[GraphemeClusterId], cursor: &TextCursor) -> Option<usize> {
5091 let trailing = usize::from(cursor.affinity == CursorAffinity::Trailing);
5092 if let Some(idx) = stops.iter().position(|id| *id == cursor.cluster_id) {
5093 return Some(idx + trailing);
5094 }
5095 let key = (cursor.cluster_id.source_run, cursor.cluster_id.start_byte_in_run);
5096 let idx = stops
5097 .iter()
5098 .rposition(|id| (id.source_run, id.start_byte_in_run) <= key)?;
5099 Some(idx + trailing)
5100 }
5101
5102 fn cursor_from_grapheme_offset(stops: &[GraphemeClusterId], offset: usize) -> TextCursor {
5106 let n = stops.len();
5107 if offset >= n {
5108 TextCursor { cluster_id: stops[n - 1], affinity: CursorAffinity::Trailing }
5109 } else {
5110 TextCursor { cluster_id: stops[offset], affinity: CursorAffinity::Leading }
5111 }
5112 }
5113
5114 pub fn move_cursor_left(
5119 &self,
5120 cursor: TextCursor,
5121 debug: &mut Option<Vec<String>>,
5122 ) -> TextCursor {
5123 let stops = self.grapheme_stops();
5124 if stops.is_empty() {
5125 return cursor;
5126 }
5127 let Some(offset) = Self::grapheme_caret_offset(&stops, &cursor) else {
5128 return cursor;
5129 };
5130 let moved = Self::cursor_from_grapheme_offset(&stops, offset.saturating_sub(1));
5131 if let Some(d) = debug {
5132 d.push(format!(
5133 "[Cursor] move_cursor_left: byte {} -> byte {}",
5134 cursor.cluster_id.start_byte_in_run, moved.cluster_id.start_byte_in_run
5135 ));
5136 }
5137 moved
5138 }
5139
5140 pub fn move_cursor_right(
5145 &self,
5146 cursor: TextCursor,
5147 debug: &mut Option<Vec<String>>,
5148 ) -> TextCursor {
5149 let stops = self.grapheme_stops();
5150 if stops.is_empty() {
5151 return cursor;
5152 }
5153 let Some(offset) = Self::grapheme_caret_offset(&stops, &cursor) else {
5154 return cursor;
5155 };
5156 let moved = Self::cursor_from_grapheme_offset(&stops, (offset + 1).min(stops.len()));
5157 if let Some(d) = debug {
5158 d.push(format!(
5159 "[Cursor] move_cursor_right: byte {} -> byte {}",
5160 cursor.cluster_id.start_byte_in_run, moved.cluster_id.start_byte_in_run
5161 ));
5162 }
5163 moved
5164 }
5165
5166 pub fn move_cursor_up(
5168 &self,
5169 cursor: TextCursor,
5170 goal_x: &mut Option<f32>,
5171 debug: &mut Option<Vec<String>>,
5172 ) -> TextCursor {
5173 if let Some(d) = debug {
5174 d.push(format!(
5175 "[Cursor] move_cursor_up: from byte {} (affinity {:?})",
5176 cursor.cluster_id.start_byte_in_run, cursor.affinity
5177 ));
5178 }
5179
5180 let Some(current_item) = self.items.iter().find(|i| {
5181 i.item
5182 .as_cluster()
5183 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5184 }) else {
5185 if let Some(d) = debug {
5186 d.push(format!(
5187 "[Cursor] move_cursor_up: cursor not found in items, staying at byte {}",
5188 cursor.cluster_id.start_byte_in_run
5189 ));
5190 }
5191 return cursor;
5192 };
5193
5194 if let Some(d) = debug {
5195 d.push(format!(
5196 "[Cursor] move_cursor_up: current line {}, position ({}, {})",
5197 current_item.line_index, current_item.position.x, current_item.position.y
5198 ));
5199 }
5200
5201 let target_line_idx = current_item.line_index.saturating_sub(1);
5202 if current_item.line_index == target_line_idx {
5203 if let Some(d) = debug {
5204 d.push(format!(
5205 "[Cursor] move_cursor_up: already at top line {}, staying put",
5206 current_item.line_index
5207 ));
5208 }
5209 return cursor;
5210 }
5211
5212 let current_x = goal_x.unwrap_or_else(|| {
5213 let x = match cursor.affinity {
5214 CursorAffinity::Leading => current_item.position.x,
5215 CursorAffinity::Trailing => {
5216 current_item.position.x + get_item_measure(¤t_item.item, false)
5217 }
5218 };
5219 *goal_x = Some(x);
5220 x
5221 });
5222
5223 let target_y = self
5225 .items
5226 .iter()
5227 .find(|i| i.line_index == target_line_idx)
5228 .map_or(current_item.position.y, |i| i.position.y + (i.item.bounds().height / 2.0));
5229
5230 if let Some(d) = debug {
5231 d.push(format!(
5232 "[Cursor] move_cursor_up: target line {target_line_idx}, hittesting at ({current_x}, {target_y})"
5233 ));
5234 }
5235
5236 let result = self
5237 .hittest_cursor(LogicalPosition {
5238 x: current_x,
5239 y: target_y,
5240 })
5241 .unwrap_or(cursor);
5242
5243 if let Some(d) = debug {
5244 d.push(format!(
5245 "[Cursor] move_cursor_up: result byte {} (affinity {:?})",
5246 result.cluster_id.start_byte_in_run, result.affinity
5247 ));
5248 }
5249
5250 result
5251 }
5252
5253 pub fn move_cursor_down(
5255 &self,
5256 cursor: TextCursor,
5257 goal_x: &mut Option<f32>,
5258 debug: &mut Option<Vec<String>>,
5259 ) -> TextCursor {
5260 if let Some(d) = debug {
5261 d.push(format!(
5262 "[Cursor] move_cursor_down: from byte {} (affinity {:?})",
5263 cursor.cluster_id.start_byte_in_run, cursor.affinity
5264 ));
5265 }
5266
5267 let Some(current_item) = self.items.iter().find(|i| {
5268 i.item
5269 .as_cluster()
5270 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5271 }) else {
5272 if let Some(d) = debug {
5273 d.push(format!(
5274 "[Cursor] move_cursor_down: cursor not found in items, staying at byte {}",
5275 cursor.cluster_id.start_byte_in_run
5276 ));
5277 }
5278 return cursor;
5279 };
5280
5281 if let Some(d) = debug {
5282 d.push(format!(
5283 "[Cursor] move_cursor_down: current line {}, position ({}, {})",
5284 current_item.line_index, current_item.position.x, current_item.position.y
5285 ));
5286 }
5287
5288 let max_line = self.items.iter().map(|i| i.line_index).max().unwrap_or(0);
5289 let target_line_idx = (current_item.line_index + 1).min(max_line);
5290 if current_item.line_index == target_line_idx {
5291 if let Some(d) = debug {
5292 d.push(format!(
5293 "[Cursor] move_cursor_down: already at bottom line {}, staying put",
5294 current_item.line_index
5295 ));
5296 }
5297 return cursor;
5298 }
5299
5300 let current_x = goal_x.unwrap_or_else(|| {
5301 let x = match cursor.affinity {
5302 CursorAffinity::Leading => current_item.position.x,
5303 CursorAffinity::Trailing => {
5304 current_item.position.x + get_item_measure(¤t_item.item, false)
5305 }
5306 };
5307 *goal_x = Some(x);
5308 x
5309 });
5310
5311 let target_y = self
5312 .items
5313 .iter()
5314 .find(|i| i.line_index == target_line_idx)
5315 .map_or(current_item.position.y, |i| i.position.y + (i.item.bounds().height / 2.0));
5316
5317 if let Some(d) = debug {
5318 d.push(format!(
5319 "[Cursor] move_cursor_down: hit testing at ({current_x}, {target_y})"
5320 ));
5321 }
5322
5323 let result = self
5324 .hittest_cursor(LogicalPosition {
5325 x: current_x,
5326 y: target_y,
5327 })
5328 .unwrap_or(cursor);
5329
5330 if let Some(d) = debug {
5331 d.push(format!(
5332 "[Cursor] move_cursor_down: result byte {}, affinity {:?}",
5333 result.cluster_id.start_byte_in_run, result.affinity
5334 ));
5335 }
5336
5337 result
5338 }
5339
5340 pub fn move_cursor_to_line_start(
5342 &self,
5343 cursor: TextCursor,
5344 debug: &mut Option<Vec<String>>,
5345 ) -> TextCursor {
5346 if let Some(d) = debug {
5347 d.push(format!(
5348 "[Cursor] move_cursor_to_line_start: starting at byte {}, affinity {:?}",
5349 cursor.cluster_id.start_byte_in_run, cursor.affinity
5350 ));
5351 }
5352
5353 let Some(current_item) = self.items.iter().find(|i| {
5354 i.item
5355 .as_cluster()
5356 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5357 }) else {
5358 if let Some(d) = debug {
5359 d.push(format!(
5360 "[Cursor] move_cursor_to_line_start: cursor not found, staying at byte {}",
5361 cursor.cluster_id.start_byte_in_run
5362 ));
5363 }
5364 return cursor;
5365 };
5366
5367 if let Some(d) = debug {
5368 d.push(format!(
5369 "[Cursor] move_cursor_to_line_start: current line {}, position ({}, {})",
5370 current_item.line_index, current_item.position.x, current_item.position.y
5371 ));
5372 }
5373
5374 let first_item_on_line = self
5375 .items
5376 .iter()
5377 .filter(|i| i.line_index == current_item.line_index)
5378 .min_by(|a, b| {
5379 a.position
5380 .x
5381 .partial_cmp(&b.position.x)
5382 .unwrap_or(Ordering::Equal)
5383 });
5384
5385 if let Some(item) = first_item_on_line {
5386 if let ShapedItem::Cluster(c) = &item.item {
5387 let result = TextCursor {
5388 cluster_id: c.source_cluster_id,
5389 affinity: CursorAffinity::Leading,
5390 };
5391 if let Some(d) = debug {
5392 d.push(format!(
5393 "[Cursor] move_cursor_to_line_start: result byte {}, affinity {:?}",
5394 result.cluster_id.start_byte_in_run, result.affinity
5395 ));
5396 }
5397 return result;
5398 }
5399 }
5400
5401 if let Some(d) = debug {
5402 d.push(format!(
5403 "[Cursor] move_cursor_to_line_start: no first item found, staying at byte {}",
5404 cursor.cluster_id.start_byte_in_run
5405 ));
5406 }
5407 cursor
5408 }
5409
5410 pub fn move_cursor_to_line_end(
5412 &self,
5413 cursor: TextCursor,
5414 debug: &mut Option<Vec<String>>,
5415 ) -> TextCursor {
5416 if let Some(d) = debug {
5417 d.push(format!(
5418 "[Cursor] move_cursor_to_line_end: starting at byte {}, affinity {:?}",
5419 cursor.cluster_id.start_byte_in_run, cursor.affinity
5420 ));
5421 }
5422
5423 let Some(current_item) = self.items.iter().find(|i| {
5424 i.item
5425 .as_cluster()
5426 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5427 }) else {
5428 if let Some(d) = debug {
5429 d.push(format!(
5430 "[Cursor] move_cursor_to_line_end: cursor not found, staying at byte {}",
5431 cursor.cluster_id.start_byte_in_run
5432 ));
5433 }
5434 return cursor;
5435 };
5436
5437 if let Some(d) = debug {
5438 d.push(format!(
5439 "[Cursor] move_cursor_to_line_end: current line {}, position ({}, {})",
5440 current_item.line_index, current_item.position.x, current_item.position.y
5441 ));
5442 }
5443
5444 let last_item_on_line = self
5445 .items
5446 .iter()
5447 .filter(|i| i.line_index == current_item.line_index)
5448 .max_by(|a, b| {
5449 a.position
5450 .x
5451 .partial_cmp(&b.position.x)
5452 .unwrap_or(Ordering::Equal)
5453 });
5454
5455 if let Some(item) = last_item_on_line {
5456 if let ShapedItem::Cluster(c) = &item.item {
5457 let result = TextCursor {
5458 cluster_id: c.source_cluster_id,
5459 affinity: CursorAffinity::Trailing,
5460 };
5461 if let Some(d) = debug {
5462 d.push(format!(
5463 "[Cursor] move_cursor_to_line_end: result byte {}, affinity {:?}",
5464 result.cluster_id.start_byte_in_run, result.affinity
5465 ));
5466 }
5467 return result;
5468 }
5469 }
5470
5471 if let Some(d) = debug {
5472 d.push(format!(
5473 "[Cursor] move_cursor_to_line_end: no last item found, staying at byte {}",
5474 cursor.cluster_id.start_byte_in_run
5475 ));
5476 }
5477 cursor
5478 }
5479
5480 pub fn move_cursor_to_prev_word(
5488 &self,
5489 cursor: TextCursor,
5490 debug: &mut Option<Vec<String>>,
5491 ) -> TextCursor {
5492 if let Some(d) = debug {
5493 d.push(format!(
5494 "[Cursor] move_cursor_to_prev_word: starting at byte {}, affinity {:?}",
5495 cursor.cluster_id.start_byte_in_run, cursor.affinity
5496 ));
5497 }
5498
5499 let Some(current_pos) = self.items.iter().position(|i| {
5500 i.item
5501 .as_cluster()
5502 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5503 }) else {
5504 return cursor;
5505 };
5506
5507 let mut pos = if cursor.affinity == CursorAffinity::Leading {
5509 current_pos.checked_sub(1)
5511 } else {
5512 Some(current_pos)
5514 };
5515
5516 while let Some(p) = pos {
5518 if let Some(cluster) = self.items[p].item.as_cluster() {
5519 if !cluster_is_word_boundary(cluster) {
5520 break;
5521 }
5522 }
5523 pos = p.checked_sub(1);
5524 }
5525
5526 while let Some(p) = pos {
5528 if let Some(cluster) = self.items[p].item.as_cluster() {
5529 if cluster_is_word_boundary(cluster) {
5530 if p + 1 < self.items.len() {
5532 if let Some(c) = self.items[p + 1].item.as_cluster() {
5533 return TextCursor {
5534 cluster_id: c.source_cluster_id,
5535 affinity: CursorAffinity::Leading,
5536 };
5537 }
5538 }
5539 break;
5540 }
5541 }
5542 if p == 0 {
5543 if let Some(c) = self.items[0].item.as_cluster() {
5545 return TextCursor {
5546 cluster_id: c.source_cluster_id,
5547 affinity: CursorAffinity::Leading,
5548 };
5549 }
5550 break;
5551 }
5552 pos = p.checked_sub(1);
5553 }
5554
5555 if pos.is_none() {
5557 if let Some(first) = self.get_first_cluster_cursor() {
5558 return first;
5559 }
5560 }
5561
5562 cursor
5563 }
5564
5565 pub fn move_cursor_to_next_word(
5572 &self,
5573 cursor: TextCursor,
5574 debug: &mut Option<Vec<String>>,
5575 ) -> TextCursor {
5576 if let Some(d) = debug {
5577 d.push(format!(
5578 "[Cursor] move_cursor_to_next_word: starting at byte {}, affinity {:?}",
5579 cursor.cluster_id.start_byte_in_run, cursor.affinity
5580 ));
5581 }
5582
5583 let Some(current_pos) = self.items.iter().position(|i| {
5584 i.item
5585 .as_cluster()
5586 .is_some_and(|c| c.source_cluster_id == cursor.cluster_id)
5587 }) else {
5588 return cursor;
5589 };
5590
5591 let len = self.items.len();
5592
5593 let start = if cursor.affinity == CursorAffinity::Trailing {
5595 current_pos + 1
5596 } else {
5597 current_pos
5598 };
5599
5600 if start >= len {
5601 return cursor;
5602 }
5603
5604 let mut pos = start;
5605
5606 while pos < len {
5608 if let Some(cluster) = self.items[pos].item.as_cluster() {
5609 if cluster_is_word_boundary(cluster) {
5610 break;
5611 }
5612 }
5613 pos += 1;
5614 }
5615
5616 while pos < len {
5618 if let Some(cluster) = self.items[pos].item.as_cluster() {
5619 if !cluster_is_word_boundary(cluster) {
5620 return TextCursor {
5622 cluster_id: cluster.source_cluster_id,
5623 affinity: CursorAffinity::Leading,
5624 };
5625 }
5626 }
5627 pos += 1;
5628 }
5629
5630 if let Some(last) = self.get_last_cluster_cursor() {
5632 return last;
5633 }
5634
5635 cursor
5636 }
5637}
5638
5639#[allow(clippy::match_same_arms)] fn get_baseline_for_item(item: &ShapedItem) -> Option<f32> {
5641 match item {
5642 ShapedItem::CombinedBlock {
5643 baseline_offset, ..
5644 } => Some(*baseline_offset),
5645 ShapedItem::Object {
5646 baseline_offset, ..
5647 } => Some(*baseline_offset),
5648 ShapedItem::Cluster(ref cluster) => {
5650 cluster.glyphs.last().map(|last_glyph| last_glyph
5651 .font_metrics
5652 .baseline_scaled(last_glyph.style.font_size_px))
5653 }
5654 ShapedItem::Break { source, break_info } => {
5655 None
5657 }
5658 ShapedItem::Tab { source, bounds } => {
5659 None
5661 }
5662 }
5663}
5664
5665#[derive(Debug, Clone, Default)]
5667pub struct OverflowInfo {
5668 pub overflow_items: Vec<ShapedItem>,
5676 pub unclipped_bounds: Rect,
5680}
5681
5682impl OverflowInfo {
5683 #[must_use] pub const fn has_overflow(&self) -> bool {
5684 !self.overflow_items.is_empty()
5685 }
5686}
5687
5688#[derive(Debug, Clone)]
5690pub struct UnifiedLine {
5691 pub items: Vec<ShapedItem>,
5692 pub cross_axis_position: f32,
5694 pub constraints: LineConstraints,
5696 pub is_last: bool,
5697}
5698
5699pub type CacheId = u64;
5702
5703#[derive(Debug, Clone)]
5705pub struct LayoutFragment {
5706 pub id: String,
5708 pub constraints: UnifiedConstraints,
5710}
5711
5712#[derive(Debug, Clone)]
5714pub(crate) struct FlowLayout {
5715 pub(crate) fragment_layouts: HashMap<String, Arc<UnifiedLayout>>,
5717 pub(crate) remaining_items: Vec<ShapedItem>,
5720}
5721
5722#[derive(Copy, Debug, Clone, Default)]
5732pub struct IntrinsicTextSizes {
5733 pub min_content_width: f32,
5735 pub max_content_width: f32,
5737 pub max_content_height: f32,
5739}
5740
5741#[derive(Clone, Debug)]
5747pub struct CachedLineBreaks {
5748 pub line_ranges: Vec<(usize, usize)>,
5750 pub line_widths: Vec<f32>,
5752 pub available_width: f32,
5754}
5755
5756#[derive(Copy, Clone, Debug)]
5758pub enum IncrementalRelayoutResult {
5759 GlyphSwap,
5761 LineShift {
5763 affected_item: usize,
5765 delta: f32,
5767 },
5768 PartialReflow {
5770 reflow_from_line: usize,
5772 },
5773 FullRelayout,
5775}
5776
5777#[must_use] pub fn extract_line_breaks(
5779 items: &[PositionedItem],
5780 available_width: f32,
5781) -> CachedLineBreaks {
5782 let mut line_ranges = Vec::new();
5783 let mut line_widths = Vec::new();
5784
5785 if items.is_empty() {
5786 return CachedLineBreaks { line_ranges, line_widths, available_width };
5787 }
5788
5789 let mut line_start = 0usize;
5790 let mut current_line = items[0].line_index;
5791 let mut line_width = 0.0f32;
5792
5793 for (i, item) in items.iter().enumerate() {
5794 if item.line_index != current_line {
5795 line_ranges.push((line_start, i));
5796 line_widths.push(line_width);
5797 line_start = i;
5798 current_line = item.line_index;
5799 line_width = 0.0;
5800 }
5801 line_width += get_item_measure(&item.item, false);
5802 }
5803
5804 line_ranges.push((line_start, items.len()));
5806 line_widths.push(line_width);
5807
5808 CachedLineBreaks { line_ranges, line_widths, available_width }
5809}
5810
5811#[must_use] pub fn try_incremental_relayout(
5818 dirty_item_indices: &[usize],
5819 old_advances: &[f32],
5820 new_advances: &[f32],
5821 line_breaks: &CachedLineBreaks,
5822) -> IncrementalRelayoutResult {
5823 if dirty_item_indices.is_empty() {
5824 return IncrementalRelayoutResult::GlyphSwap;
5825 }
5826
5827 for &dirty_idx in dirty_item_indices {
5829 if dirty_idx >= old_advances.len() || dirty_idx >= new_advances.len() {
5830 return IncrementalRelayoutResult::FullRelayout;
5831 }
5832
5833 let old_adv = old_advances[dirty_idx];
5834 let new_adv = new_advances[dirty_idx];
5835 let delta = new_adv - old_adv;
5836
5837 if delta.abs() < 0.001 {
5838 continue;
5840 }
5841
5842 let line_idx = line_breaks.line_ranges.iter()
5844 .position(|&(start, end)| dirty_idx >= start && dirty_idx < end);
5845
5846 let Some(line_idx) = line_idx else {
5847 return IncrementalRelayoutResult::FullRelayout;
5848 };
5849
5850 let old_line_width = line_breaks.line_widths[line_idx];
5851 let new_line_width = old_line_width + delta;
5852
5853 if new_line_width <= line_breaks.available_width {
5854 return IncrementalRelayoutResult::LineShift {
5856 affected_item: dirty_idx,
5857 delta,
5858 };
5859 }
5860 return IncrementalRelayoutResult::PartialReflow {
5862 reflow_from_line: line_idx,
5863 };
5864 }
5865
5866 IncrementalRelayoutResult::GlyphSwap
5868}
5869
5870#[derive(Debug)]
5873pub(crate) struct PerItemShapedEntry {
5874 pub(crate) clusters: Vec<ShapedItem>,
5876 pub(crate) total_advance: f32,
5878}
5879
5880#[derive(Debug)]
5881pub struct TextShapingCache {
5882 logical_items: HashMap<CacheId, Arc<Vec<LogicalItem>>>,
5884 visual_items: HashMap<CacheId, Arc<Vec<VisualItem>>>,
5886 shaped_items: HashMap<CacheId, Arc<Vec<ShapedItem>>>,
5888 per_item_shaped: HashMap<u64, Arc<PerItemShapedEntry>>,
5891 per_item_accessed: HashSet<u64>,
5893 generation: u64,
5895}
5896
5897#[derive(Copy, Debug, Clone, Default)]
5899pub struct TextCacheMemoryReport {
5900 pub logical_items_entries: usize,
5901 pub logical_items_bytes: usize,
5902 pub visual_items_entries: usize,
5903 pub visual_items_bytes: usize,
5904 pub shaped_items_entries: usize,
5905 pub shaped_items_bytes: usize,
5906 pub shaped_glyph_bytes: usize,
5907 pub shaped_cluster_text_bytes: usize,
5908 pub per_item_shaped_entries: usize,
5909 pub per_item_shaped_bytes: usize,
5910}
5911
5912impl TextCacheMemoryReport {
5913 #[must_use] pub const fn total_bytes(&self) -> usize {
5914 self.logical_items_bytes
5915 + self.visual_items_bytes
5916 + self.shaped_items_bytes
5917 + self.shaped_glyph_bytes
5918 + self.shaped_cluster_text_bytes
5919 + self.per_item_shaped_bytes
5920 }
5921}
5922
5923impl TextShapingCache {
5924 #[must_use] pub fn new() -> Self {
5925 Self {
5926 logical_items: HashMap::new(),
5927 visual_items: HashMap::new(),
5928 shaped_items: HashMap::new(),
5929 per_item_shaped: HashMap::new(),
5930 per_item_accessed: HashSet::new(),
5931 generation: 0,
5932 }
5933 }
5934
5935 #[allow(clippy::field_reassign_with_default)] #[must_use] pub fn memory_report(&self) -> TextCacheMemoryReport {
5938 let mut r = TextCacheMemoryReport::default();
5939 r.logical_items_entries = self.logical_items.len();
5940 for arc in self.logical_items.values() {
5941 r.logical_items_bytes += arc.capacity() * size_of::<LogicalItem>();
5942 }
5943 r.visual_items_entries = self.visual_items.len();
5944 for arc in self.visual_items.values() {
5945 r.visual_items_bytes += arc.capacity() * size_of::<VisualItem>();
5946 }
5947 r.shaped_items_entries = self.shaped_items.len();
5948 for arc in self.shaped_items.values() {
5949 r.shaped_items_bytes += arc.capacity() * size_of::<ShapedItem>();
5950 for item in arc.iter() {
5951 if let ShapedItem::Cluster(c) = item {
5952 r.shaped_glyph_bytes += c.glyphs.capacity() * size_of::<ShapedGlyph>();
5953 r.shaped_cluster_text_bytes += c.text.capacity();
5954 }
5955 }
5956 }
5957 r.per_item_shaped_entries = self.per_item_shaped.len();
5958 for arc in self.per_item_shaped.values() {
5959 r.per_item_shaped_bytes += arc.clusters.capacity() * size_of::<ShapedItem>();
5960 for item in &arc.clusters {
5961 if let ShapedItem::Cluster(c) = item {
5962 r.per_item_shaped_bytes += c.glyphs.capacity() * size_of::<ShapedGlyph>();
5963 r.per_item_shaped_bytes += c.text.capacity();
5964 }
5965 }
5966 }
5967 r
5968 }
5969
5970 pub fn begin_generation(&mut self) {
5973 if self.generation > 0 && !self.per_item_accessed.is_empty() {
5974 let accessed = &self.per_item_accessed;
5976 self.per_item_shaped.retain(|k, _| accessed.contains(k));
5977 }
5978 self.per_item_accessed.clear();
5979 self.generation += 1;
5980 }
5981
5982 #[must_use] pub fn use_old_layout(
5997 old_constraints: &UnifiedConstraints,
5998 new_constraints: &UnifiedConstraints,
5999 old_content: &[InlineContent],
6000 new_content: &[InlineContent],
6001 ) -> bool {
6002 if old_constraints != new_constraints {
6004 return false;
6005 }
6006
6007 if old_content.len() != new_content.len() {
6009 return false;
6010 }
6011
6012 for (old, new) in old_content.iter().zip(new_content.iter()) {
6014 if !Self::inline_content_layout_eq(old, new) {
6015 return false;
6016 }
6017 }
6018
6019 true
6020 }
6021
6022 fn inline_content_layout_eq(old: &InlineContent, new: &InlineContent) -> bool {
6026 use InlineContent::{Text, Image, Space, LineBreak, Tab, Marker, Shape, Ruby};
6027 match (old, new) {
6028 (Text(old_run), Text(new_run)) => {
6029 old_run.text == new_run.text
6031 && old_run.style.layout_eq(&new_run.style)
6032 }
6033 (Image(old_img), Image(new_img)) => {
6034 old_img.intrinsic_size == new_img.intrinsic_size
6036 && old_img.display_size == new_img.display_size
6037 && old_img.baseline_offset == new_img.baseline_offset
6038 && old_img.alignment == new_img.alignment
6039 }
6040 (Space(old_sp), Space(new_sp)) => old_sp == new_sp,
6041 (LineBreak(old_br), LineBreak(new_br)) => old_br == new_br,
6042 (Tab { style: old_style }, Tab { style: new_style }) => old_style.layout_eq(new_style),
6043 (Marker { run: old_run, position_outside: old_pos },
6044 Marker { run: new_run, position_outside: new_pos }) => {
6045 old_pos == new_pos
6046 && old_run.text == new_run.text
6047 && old_run.style.layout_eq(&new_run.style)
6048 }
6049 (Shape(old_shape), Shape(new_shape)) => {
6050 old_shape.shape_def == new_shape.shape_def
6052 && old_shape.baseline_offset == new_shape.baseline_offset
6053 }
6054 (Ruby { base: old_base, text: old_text, style: old_style },
6055 Ruby { base: new_base, text: new_text, style: new_style }) => {
6056 old_style.layout_eq(new_style)
6057 && old_base.len() == new_base.len()
6058 && old_text.len() == new_text.len()
6059 && old_base.iter().zip(new_base.iter())
6060 .all(|(o, n)| Self::inline_content_layout_eq(o, n))
6061 && old_text.iter().zip(new_text.iter())
6062 .all(|(o, n)| Self::inline_content_layout_eq(o, n))
6063 }
6064 _ => false,
6066 }
6067 }
6068}
6069
6070impl Default for TextShapingCache {
6071 fn default() -> Self {
6072 Self::new()
6073 }
6074}
6075
6076#[derive(Debug, Clone, Eq, PartialEq, Hash)]
6078pub(crate) struct LogicalItemsKey<'a> {
6079 pub(crate) inline_content_hash: u64,
6080 pub(crate) default_font_size: u32,
6081 pub(crate) _marker: std::marker::PhantomData<&'a ()>,
6082}
6083
6084#[derive(Debug, Clone, Eq, PartialEq, Hash)]
6086pub(crate) struct VisualItemsKey {
6087 pub(crate) logical_items_id: CacheId,
6088 pub(crate) base_direction: BidiDirection,
6089}
6090
6091#[derive(Debug, Clone, Eq, PartialEq, Hash)]
6093pub(crate) struct ShapedItemsKey {
6094 pub(crate) visual_items_id: CacheId,
6095 pub(crate) style_hash: u64,
6096}
6097
6098impl ShapedItemsKey {
6099 pub(crate) fn new(visual_items_id: CacheId, visual_items: &[VisualItem]) -> Self {
6100 let style_hash = {
6101 let mut hasher = DefaultHasher::new();
6102 for item in visual_items {
6103 match &item.logical_source {
6105 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
6106 style.as_ref().hash(&mut hasher);
6107 }
6108 _ => {}
6109 }
6110 }
6111 hasher.finish()
6112 };
6113
6114 Self {
6115 visual_items_id,
6116 style_hash,
6117 }
6118 }
6119}
6120
6121#[derive(Debug, Clone, Eq, PartialEq, Hash)]
6123pub(crate) struct LayoutKey {
6124 pub(crate) shaped_items_id: CacheId,
6125 pub(crate) constraints: UnifiedConstraints,
6126}
6127
6128fn calculate_id<T: Hash>(item: &T) -> CacheId {
6130 let mut hasher = DefaultHasher::new();
6131 item.hash(&mut hasher);
6132 hasher.finish()
6133}
6134
6135impl TextShapingCache {
6138 #[allow(clippy::too_many_lines)] pub fn layout_flow<T: ParsedFontTrait>(
6203 &mut self,
6204 content: &[InlineContent],
6205 style_overrides: &[StyleOverride],
6206 flow_chain: &[LayoutFragment],
6207 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
6208 fc_cache: &FcFontCache,
6209 loaded_fonts: &LoadedFonts<T>,
6210 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6211 ) -> Result<FlowLayout, LayoutError> {
6212 #[cfg(feature = "web_lift")]
6214 unsafe {
6215 crate::az_mark((0x60BD0) as u32, (content.as_ptr() as usize as u32) as u32);
6216 crate::az_mark((0x60BD4) as u32, (content.len() as u32 | 0xC0DE0000) as u32);
6217 }
6218 const PER_ITEM_CACHE_MAX: usize = 4096;
6231 if self.per_item_shaped.len() > PER_ITEM_CACHE_MAX {
6232 self.begin_generation();
6233 }
6234
6235 let logical_items_id = calculate_id(&content);
6245 let logical_items = self
6246 .logical_items
6247 .entry(logical_items_id)
6248 .or_insert_with(|| {
6249 Arc::new(create_logical_items(content, style_overrides, debug_messages))
6250 })
6251 .clone();
6252
6253 let default_constraints = UnifiedConstraints::default();
6256 let first_constraints = flow_chain
6257 .first()
6258 .map_or(&default_constraints, |f| &f.constraints);
6259
6260 let unicode_bidi_val = first_constraints.unicode_bidi;
6272 let base_direction = if unicode_bidi_val == UnicodeBidi::Plaintext {
6273 let has_strong = logical_items.iter().any(|item| {
6275 if let LogicalItem::Text { text, .. } = item {
6276 matches!(unicode_bidi::get_base_direction(text.as_str()),
6277 unicode_bidi::Direction::Ltr | unicode_bidi::Direction::Rtl)
6278 } else {
6279 false
6280 }
6281 });
6282 if has_strong {
6283 get_base_direction_from_logical(&logical_items)
6284 } else {
6285 first_constraints.direction.unwrap_or(BidiDirection::Ltr)
6287 }
6288 } else {
6289 first_constraints.direction.unwrap_or(BidiDirection::Ltr)
6291 };
6292 let visual_key = VisualItemsKey {
6293 logical_items_id,
6294 base_direction,
6295 };
6296 let visual_items_id = calculate_id(&visual_key);
6297 let visual_items = self
6300 .visual_items
6301 .entry(visual_items_id)
6302 .or_insert_with(|| {
6303 Arc::new(
6304 reorder_logical_items(&logical_items, base_direction, unicode_bidi_val, debug_messages).unwrap(),
6305 )
6306 })
6307 .clone();
6308
6309 let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
6312 let shaped_items_id = calculate_id(&shaped_key);
6313 let shaped_items = if let Some(cached) = self.shaped_items.get(&shaped_items_id) {
6315 cached.clone()
6317 } else {
6318 let items = Arc::new(shape_visual_items_with_per_item_cache(
6321 &visual_items,
6322 &mut self.per_item_shaped,
6323 &mut self.per_item_accessed,
6324 font_chain_cache,
6325 fc_cache,
6326 loaded_fonts,
6327 debug_messages,
6328 )?);
6329 self.shaped_items.insert(shaped_items_id, items.clone());
6330 items
6331 };
6332
6333 let oriented_items = apply_text_orientation(shaped_items, first_constraints);
6340
6341 let mut fragment_layouts = HashMap::new();
6343 let mut cursor = BreakCursor::with_word_break(&oriented_items, first_constraints.word_break);
6346 cursor.hyphens = first_constraints.hyphenation;
6347 cursor.line_break = first_constraints.line_break;
6348
6349 #[allow(clippy::no_effect_underscore_binding)] let mut _az_flow_iters: usize = 0;
6356 for fragment in flow_chain {
6357 #[cfg(feature = "web_lift")]
6358 {
6359 _az_flow_iters += 1;
6360 unsafe { crate::az_mark((0x60BC0) as u32, (_az_flow_iters as u32 | 0xC0DE0000) as u32); }
6361 if _az_flow_iters > 256 {
6362 break;
6363 }
6364 }
6365 let fragment_layout = perform_fragment_layout(
6367 &mut cursor,
6368 &logical_items,
6369 &fragment.constraints,
6370 debug_messages,
6371 loaded_fonts,
6372 )?;
6373
6374 fragment_layouts.insert(fragment.id.clone(), Arc::new(fragment_layout));
6375 if cursor.is_done() {
6376 break; }
6378 }
6379
6380 Ok(FlowLayout {
6381 fragment_layouts,
6382 remaining_items: cursor.drain_remaining(),
6383 })
6384 }
6385
6386 #[allow(clippy::too_many_lines)] pub fn measure_intrinsic_widths<T: ParsedFontTrait>(
6409 &mut self,
6410 content: &[InlineContent],
6411 style_overrides: &[StyleOverride],
6412 constraints: &UnifiedConstraints,
6413 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
6414 fc_cache: &FcFontCache,
6415 loaded_fonts: &LoadedFonts<T>,
6416 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6417 ) -> Result<IntrinsicTextSizes, LayoutError> {
6418 const PER_ITEM_CACHE_MAX: usize = 4096;
6419 if self.per_item_shaped.len() > PER_ITEM_CACHE_MAX {
6420 self.begin_generation();
6421 }
6422
6423 let logical_items_id = calculate_id(&content);
6427 let logical_items = self
6428 .logical_items
6429 .entry(logical_items_id)
6430 .or_insert_with(|| {
6431 Arc::new(create_logical_items(content, style_overrides, debug_messages))
6432 })
6433 .clone();
6434
6435 let unicode_bidi_val = constraints.unicode_bidi;
6437 let base_direction = if unicode_bidi_val == UnicodeBidi::Plaintext {
6438 let has_strong = logical_items.iter().any(|item| {
6439 if let LogicalItem::Text { text, .. } = item {
6440 matches!(unicode_bidi::get_base_direction(text.as_str()),
6441 unicode_bidi::Direction::Ltr | unicode_bidi::Direction::Rtl)
6442 } else {
6443 false
6444 }
6445 });
6446 if has_strong {
6447 get_base_direction_from_logical(&logical_items)
6448 } else {
6449 constraints.direction.unwrap_or(BidiDirection::Ltr)
6450 }
6451 } else {
6452 constraints.direction.unwrap_or(BidiDirection::Ltr)
6453 };
6454 let visual_key = VisualItemsKey {
6455 logical_items_id,
6456 base_direction,
6457 };
6458 let visual_items_id = calculate_id(&visual_key);
6459 let visual_items = self
6460 .visual_items
6461 .entry(visual_items_id)
6462 .or_insert_with(|| {
6463 Arc::new(
6464 reorder_logical_items(&logical_items, base_direction, unicode_bidi_val, debug_messages).unwrap(),
6465 )
6466 })
6467 .clone();
6468
6469 let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
6471 let shaped_items_id = calculate_id(&shaped_key);
6472 let shaped_items = if let Some(cached) = self.shaped_items.get(&shaped_items_id) { cached.clone() } else {
6473 let items = Arc::new(shape_visual_items_with_per_item_cache(
6474 &visual_items,
6475 &mut self.per_item_shaped,
6476 &mut self.per_item_accessed,
6477 font_chain_cache,
6478 fc_cache,
6479 loaded_fonts,
6480 debug_messages,
6481 )?);
6482 self.shaped_items.insert(shaped_items_id, items.clone());
6483 items
6484 };
6485
6486 let oriented_items = apply_text_orientation(shaped_items, constraints);
6488
6489 let word_break = constraints.word_break;
6491 let hyphens = constraints.hyphenation;
6492
6493 let mut total = 0.0f32; let mut max_line = 0.0f32; let mut max_word = 0.0f32;
6496 let mut cur_word = 0.0f32;
6497 let mut max_line_height = 0.0f32;
6498
6499 for item in oriented_items.iter() {
6500 if let ShapedItem::Break { .. } = item {
6506 if total > max_line { max_line = total; }
6507 if cur_word > max_word { max_word = cur_word; }
6508 total = 0.0;
6509 cur_word = 0.0;
6510 continue;
6511 }
6512 let advance = match item {
6521 ShapedItem::Cluster(c) => {
6522 let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
6523 let mut a = c.advance + total_kerning;
6524 if !is_cursive_script_cluster(c) {
6529 a += c.style.letter_spacing.resolve_px(c.style.font_size_px);
6530 }
6531 if is_word_separator(item) {
6532 a += c.style.word_spacing.resolve_px(c.style.font_size_px);
6533 }
6534 a
6535 }
6536 ShapedItem::CombinedBlock { bounds, .. }
6537 | ShapedItem::Object { bounds, .. }
6538 | ShapedItem::Tab { bounds, .. } => bounds.width,
6539 ShapedItem::Break { .. } => 0.0,
6540 };
6541 let adv = advance.max(0.0);
6542 total += adv;
6543
6544 let (asc, desc) = get_item_vertical_metrics_approx(item);
6545 let h = (asc + desc).max(item.bounds().height);
6546 if h > max_line_height {
6547 max_line_height = h;
6548 }
6549
6550 if is_break_opportunity_with_word_break(item, word_break, hyphens) {
6551 if cur_word > max_word {
6552 max_word = cur_word;
6553 }
6554 if !is_word_separator(item) && adv > max_word {
6561 max_word = adv;
6562 }
6563 cur_word = 0.0;
6564 } else {
6565 cur_word += adv;
6566 }
6567 }
6568 if cur_word > max_word {
6569 max_word = cur_word;
6570 }
6571 if total > max_line {
6572 max_line = total;
6573 }
6574
6575 let min_content_width = if matches!(constraints.white_space_mode, WhiteSpaceMode::Nowrap) {
6580 max_line
6581 } else {
6582 max_word
6583 };
6584
6585 Ok(IntrinsicTextSizes {
6586 min_content_width,
6587 max_content_width: max_line,
6588 max_content_height: max_line_height,
6589 })
6590 }
6591}
6592
6593#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn create_logical_items(
6600 content: &[InlineContent],
6601 style_overrides: &[StyleOverride],
6602 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6603) -> Vec<LogicalItem> {
6604 if let Some(msgs) = debug_messages {
6605 msgs.push(LayoutDebugMessage::info(
6606 "\n--- Entering create_logical_items (Refactored) ---".to_string(),
6607 ));
6608 msgs.push(LayoutDebugMessage::info(format!(
6609 "Input content length: {}",
6610 content.len()
6611 )));
6612 msgs.push(LayoutDebugMessage::info(format!(
6613 "Input overrides length: {}",
6614 style_overrides.len()
6615 )));
6616 }
6617
6618 let mut items: Vec<LogicalItem> = Vec::new();
6619 let mut style_cache: HashMap<u64, Arc<StyleProperties>> = HashMap::new();
6620
6621 let mut run_overrides: HashMap<u32, HashMap<u32, &PartialStyleProperties>> = HashMap::new();
6623 for override_item in style_overrides {
6624 run_overrides
6625 .entry(override_item.target.run_index)
6626 .or_default()
6627 .insert(override_item.target.item_index, &override_item.style);
6628 }
6629
6630 for (run_idx, inline_item) in content.iter().enumerate() {
6631 if let Some(msgs) = debug_messages {
6632 msgs.push(LayoutDebugMessage::info(format!(
6633 "Processing content run #{run_idx}"
6634 )));
6635 }
6636
6637 let marker_position_outside = match inline_item {
6639 InlineContent::Marker {
6640 position_outside, ..
6641 } => Some(*position_outside),
6642 _ => None,
6643 };
6644
6645 if let InlineContent::Text(run) | InlineContent::Marker { run, .. } = inline_item {
6653 let text = &run.text;
6654 if text.is_empty() {
6655 if let Some(msgs) = debug_messages {
6656 msgs.push(LayoutDebugMessage::info(
6657 " Run is empty, skipping.".to_string(),
6658 ));
6659 }
6660 continue;
6661 }
6662 if let Some(msgs) = debug_messages {
6663 msgs.push(LayoutDebugMessage::info(format!(" Run text: '{text}'")));
6664 }
6665
6666 let current_run_overrides = run_overrides.get(&(run_idx as u32));
6667 let mut boundaries = BTreeSet::new();
6668 boundaries.insert(0);
6669 boundaries.insert(text.len());
6670
6671 let needs_scan = current_run_overrides.is_some()
6679 || run.style.text_combine_upright.is_some();
6680 let mut scan_cursor = 0;
6681 while needs_scan && scan_cursor < text.len() {
6682 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));
6683
6684 let current_char = text[scan_cursor..].chars().next().unwrap();
6685
6686 if let Some(TextCombineUpright::Digits(max_digits)) =
6691 style_at_cursor.text_combine_upright
6692 {
6693 if max_digits > 0 && current_char.is_ascii_digit() {
6694 let digit_chunk: String = text[scan_cursor..]
6695 .chars()
6696 .take(max_digits as usize)
6697 .take_while(char::is_ascii_digit)
6698 .collect();
6699
6700 let end_of_chunk = scan_cursor + digit_chunk.len();
6701 boundaries.insert(scan_cursor);
6702 boundaries.insert(end_of_chunk);
6703 scan_cursor = end_of_chunk; continue;
6705 }
6706 }
6707
6708 if current_run_overrides
6711 .and_then(|o| o.get(&(scan_cursor as u32)))
6712 .is_some()
6713 {
6714 let grapheme_len = text[scan_cursor..]
6715 .graphemes(true)
6716 .next()
6717 .unwrap_or("")
6718 .len();
6719 boundaries.insert(scan_cursor);
6720 boundaries.insert(scan_cursor + grapheme_len);
6721 scan_cursor += grapheme_len;
6722 continue;
6723 }
6724
6725 scan_cursor += current_char.len_utf8();
6728 }
6729
6730 if let Some(msgs) = debug_messages {
6731 msgs.push(LayoutDebugMessage::info(format!(
6732 " Boundaries: {boundaries:?}"
6733 )));
6734 }
6735
6736 for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
6738 let (start, end) = (*start, *end);
6739 if start >= end {
6740 continue;
6741 }
6742
6743 let text_slice = &text[start..end];
6744 if let Some(msgs) = debug_messages {
6745 msgs.push(LayoutDebugMessage::info(format!(
6746 " Processing chunk from {start} to {end}: '{text_slice}'"
6747 )));
6748 }
6749
6750 let style_to_use = current_run_overrides.and_then(|o| o.get(&(start as u32))).map_or_else(|| run.style.clone(), |partial_style| {
6751 if let Some(msgs) = debug_messages {
6752 msgs.push(LayoutDebugMessage::info(format!(
6753 " -> Applying override at byte {start}"
6754 )));
6755 }
6756 let mut hasher = DefaultHasher::new();
6757 Arc::as_ptr(&run.style).hash(&mut hasher);
6758 partial_style.hash(&mut hasher);
6759 style_cache
6760 .entry(hasher.finish())
6761 .or_insert_with(|| Arc::new(run.style.apply_override(partial_style)))
6762 .clone()
6763 });
6764
6765 let is_combinable_chunk = match &style_to_use.text_combine_upright {
6772 Some(TextCombineUpright::All) => !text_slice.is_empty(),
6773 Some(TextCombineUpright::Digits(max_digits)) => {
6774 *max_digits > 0
6775 && !text_slice.is_empty()
6776 && text_slice.chars().all(|c| c.is_ascii_digit())
6777 && text_slice.chars().count() <= *max_digits as usize
6778 }
6779 _ => false,
6780 };
6781
6782 if is_combinable_chunk {
6783 let trimmed = text_slice.trim();
6785 let combined_text = if trimmed.is_empty() {
6786 text_slice.to_string()
6787 } else {
6788 trimmed.to_string()
6789 };
6790 items.push(LogicalItem::CombinedText {
6791 source: ContentIndex {
6792 run_index: run_idx as u32,
6793 item_index: start as u32,
6794 },
6795 text: combined_text,
6796 style: style_to_use,
6797 });
6798 } else {
6799 items.push(LogicalItem::Text {
6800 source: ContentIndex {
6801 run_index: run_idx as u32,
6802 item_index: start as u32,
6803 },
6804 text: text_slice.to_string(),
6805 style: style_to_use,
6806 marker_position_outside,
6807 source_node_id: run.source_node_id,
6808 });
6809 }
6810 }
6811 } else {
6812 match inline_item {
6813 InlineContent::LineBreak(break_info) => {
6815 if let Some(msgs) = debug_messages {
6816 msgs.push(LayoutDebugMessage::info(format!(
6817 " LineBreak: {break_info:?}"
6818 )));
6819 }
6820 items.push(LogicalItem::Break {
6821 source: ContentIndex {
6822 run_index: run_idx as u32,
6823 item_index: 0,
6824 },
6825 break_info: *break_info,
6826 });
6827 }
6828 InlineContent::Tab { style } => {
6830 if let Some(msgs) = debug_messages {
6831 msgs.push(LayoutDebugMessage::info(" Tab character".to_string()));
6832 }
6833 items.push(LogicalItem::Tab {
6834 source: ContentIndex {
6835 run_index: run_idx as u32,
6836 item_index: 0,
6837 },
6838 style: style.clone(),
6839 });
6840 }
6841 _ => {
6844 if let Some(msgs) = debug_messages {
6845 msgs.push(LayoutDebugMessage::info(
6846 " Run is not text, creating generic LogicalItem.".to_string(),
6847 ));
6848 }
6849 items.push(LogicalItem::Object {
6850 source: ContentIndex {
6851 run_index: run_idx as u32,
6852 item_index: 0,
6853 },
6854 content: inline_item.clone(),
6855 });
6856 }
6857 }
6858 }
6859 }
6860 if let Some(msgs) = debug_messages {
6861 msgs.push(LayoutDebugMessage::info(format!(
6862 "--- Exiting create_logical_items, created {} items ---",
6863 items.len()
6864 )));
6865 }
6866 items
6867}
6868
6869#[must_use] pub fn get_base_direction_from_logical(logical_items: &[LogicalItem]) -> BidiDirection {
6875 let first_strong = logical_items.iter().find_map(|item| {
6876 if let LogicalItem::Text { text, .. } = item {
6877 Some(unicode_bidi::get_base_direction(text.as_str()))
6878 } else {
6879 None
6880 }
6881 });
6882
6883 match first_strong {
6884 Some(unicode_bidi::Direction::Rtl) => BidiDirection::Rtl,
6885 _ => BidiDirection::Ltr,
6886 }
6887}
6888
6889#[allow(clippy::match_same_arms)] #[allow(clippy::too_many_lines)] pub fn reorder_logical_items(
6906 logical_items: &[LogicalItem],
6907 base_direction: BidiDirection,
6908 unicode_bidi: UnicodeBidi,
6909 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6910) -> Result<Vec<VisualItem>, LayoutError> {
6911 if let Some(msgs) = debug_messages {
6912 msgs.push(LayoutDebugMessage::info(
6913 "\n--- Entering reorder_logical_items ---".to_string(),
6914 ));
6915 msgs.push(LayoutDebugMessage::info(format!(
6916 "Input logical items count: {}",
6917 logical_items.len()
6918 )));
6919 msgs.push(LayoutDebugMessage::info(format!(
6920 "Base direction: {base_direction:?}"
6921 )));
6922 }
6923
6924 let mut bidi_str = String::new();
6926 let mut item_map = Vec::new();
6927 let mut logical_item_starts = Vec::with_capacity(logical_items.len());
6931 for (idx, item) in logical_items.iter().enumerate() {
6932 let text = match item {
6951 LogicalItem::Text { text, .. } => text.as_str(),
6952 LogicalItem::CombinedText { text, .. } => text.as_str(),
6953 _ => "\u{FFFC}",
6954 };
6955 let start_byte = bidi_str.len();
6956 logical_item_starts.push(start_byte);
6957 bidi_str.push_str(text);
6958 for _ in start_byte..bidi_str.len() {
6959 item_map.push(idx);
6960 }
6961 }
6962
6963 if bidi_str.is_empty() {
6964 if let Some(msgs) = debug_messages {
6965 msgs.push(LayoutDebugMessage::info(
6966 "Bidi string is empty, returning.".to_string(),
6967 ));
6968 }
6969 return Ok(Vec::new());
6970 }
6971 if let Some(msgs) = debug_messages {
6972 msgs.push(LayoutDebugMessage::info(format!(
6973 "Constructed bidi string: '{bidi_str}'"
6974 )));
6975 }
6976
6977 let bidi_level = if unicode_bidi == UnicodeBidi::Plaintext {
6982 None
6983 } else if base_direction == BidiDirection::Rtl {
6984 Some(Level::rtl())
6985 } else {
6986 Some(Level::ltr())
6987 };
6988 let bidi_info = BidiInfo::new(&bidi_str, bidi_level);
6990 let para = &bidi_info.paragraphs[0];
6991 let (levels, visual_runs) = bidi_info.visual_runs(para, para.range.clone());
6992
6993 if let Some(msgs) = debug_messages {
6994 msgs.push(LayoutDebugMessage::info(
6995 "Bidi visual runs generated:".to_string(),
6996 ));
6997 for (i, run_range) in visual_runs.iter().enumerate() {
6998 let level = levels[run_range.start].number();
6999 let slice = &bidi_str[run_range.start..run_range.end];
7000 msgs.push(LayoutDebugMessage::info(format!(
7001 " Run {i}: range={run_range:?}, level={level}, text='{slice}'"
7002 )));
7003 }
7004 }
7005
7006 let mut visual_items = Vec::new();
7022 for run_range in visual_runs {
7023 let bidi_level = BidiLevel::new(levels[run_range.start].number());
7024 let mut sub_run_start = run_range.start;
7025
7026 for i in (run_range.start + 1)..run_range.end {
7027 if item_map[i] != item_map[sub_run_start] {
7028 let logical_idx = item_map[sub_run_start];
7029 let logical_item = &logical_items[logical_idx];
7030 let text_slice = &bidi_str[sub_run_start..i];
7031 visual_items.push(VisualItem {
7032 logical_source: logical_item.clone(),
7033 bidi_level,
7034 script: crate::text3::script::detect_script(text_slice)
7035 .unwrap_or(Script::Latin),
7036 text: text_slice.to_string(),
7037 run_byte_offset: sub_run_start - logical_item_starts[logical_idx],
7038 });
7039 sub_run_start = i;
7040 }
7041 }
7042
7043 let logical_idx = item_map[sub_run_start];
7044 let logical_item = &logical_items[logical_idx];
7045 let text_slice = &bidi_str[sub_run_start..run_range.end];
7046 visual_items.push(VisualItem {
7047 logical_source: logical_item.clone(),
7048 bidi_level,
7049 script: crate::text3::script::detect_script(text_slice).unwrap_or(Script::Latin),
7050 text: text_slice.to_string(),
7051 run_byte_offset: sub_run_start - logical_item_starts[logical_idx],
7052 });
7053 }
7054
7055 if let Some(msgs) = debug_messages {
7056 msgs.push(LayoutDebugMessage::info(
7057 "Final visual items produced:".to_string(),
7058 ));
7059 for (i, item) in visual_items.iter().enumerate() {
7060 msgs.push(LayoutDebugMessage::info(format!(
7061 " Item {}: level={}, text='{}'",
7062 i,
7063 item.bidi_level.level(),
7064 item.text
7065 )));
7066 }
7067 msgs.push(LayoutDebugMessage::info(
7068 "--- Exiting reorder_logical_items ---".to_string(),
7069 ));
7070 }
7071 Ok(visual_items)
7072}
7073
7074#[allow(clippy::implicit_hasher)] pub fn shape_visual_items_with_per_item_cache<T: ParsedFontTrait>(
7103 visual_items: &[VisualItem],
7104 per_item_cache: &mut HashMap<u64, Arc<PerItemShapedEntry>>,
7105 per_item_accessed: &mut HashSet<u64>,
7106 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
7107 fc_cache: &FcFontCache,
7108 loaded_fonts: &LoadedFonts<T>,
7109 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
7110) -> Result<Vec<ShapedItem>, LayoutError> {
7111 use std::hash::{Hash, Hasher};
7112 let mut shaped = Vec::new();
7119 let mut idx = 0;
7120
7121 while idx < visual_items.len() {
7122 let item = &visual_items[idx];
7123
7124 let (layout_hash, bidi_level, script) = match &item.logical_source {
7126 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
7127 (style.layout_hash(), item.bidi_level, item.script)
7128 }
7129 _ => {
7130 let single = shape_visual_items(
7132 &visual_items[idx..=idx],
7133 font_chain_cache, fc_cache, loaded_fonts, debug_messages,
7134 )?;
7135 shaped.extend(single);
7136 idx += 1;
7137 continue;
7138 }
7139 };
7140
7141 let mut coalesce_end = idx + 1;
7142 while coalesce_end < visual_items.len() {
7143 let next = &visual_items[coalesce_end];
7144 let next_layout_hash = match &next.logical_source {
7145 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
7146 Some(style.layout_hash())
7147 }
7148 _ => None,
7149 };
7150 if let Some(nlh) = next_layout_hash {
7151 if nlh == layout_hash
7152 && next.bidi_level == bidi_level
7153 && next.script == script
7154 {
7155 coalesce_end += 1;
7156 } else {
7157 break;
7158 }
7159 } else {
7160 break;
7161 }
7162 }
7163
7164 let mut hasher = DefaultHasher::new();
7166 for item in &visual_items[idx..coalesce_end] {
7167 item.text.hash(&mut hasher);
7168 }
7169 layout_hash.hash(&mut hasher);
7170 bidi_level.hash(&mut hasher);
7171 (script as u32).hash(&mut hasher);
7172 let group_key = hasher.finish();
7173
7174 per_item_accessed.insert(group_key);
7176 if let Some(cached) = per_item_cache.get(&group_key) {
7177 shaped.extend(cached.clusters.iter().cloned());
7178 } else {
7179 let group_items = shape_visual_items(
7181 &visual_items[idx..coalesce_end],
7182 font_chain_cache, fc_cache, loaded_fonts, debug_messages,
7183 )?;
7184 let total_advance: f32 = group_items.iter().map(|item| {
7185 match item {
7186 ShapedItem::Cluster(c) => c.advance,
7187 _ => 0.0,
7188 }
7189 }).sum();
7190 per_item_cache.insert(group_key, Arc::new(PerItemShapedEntry {
7191 clusters: group_items.clone(),
7192 total_advance,
7193 }));
7194 shaped.extend(group_items);
7195 }
7196
7197 idx = coalesce_end;
7198 }
7199
7200 Ok(shaped)
7201}
7202
7203fn split_text_by_font_coverage<T: ParsedFontTrait>(
7208 text: &str,
7209 font_chain: &rust_fontconfig::FontFallbackChain,
7210 fc_cache: &FcFontCache,
7211 loaded_fonts: &LoadedFonts<T>,
7212) -> Vec<(usize, usize, FontId)> {
7213 let mut segments: Vec<(usize, usize, FontId)> = Vec::new();
7214
7215 let notdef_font_id = loaded_fonts.iter().map(|(id, _)| *id).min();
7219
7220 for (byte_idx, ch) in text.char_indices() {
7221 let char_end = byte_idx + ch.len_utf8();
7222 let font_id = font_chain
7228 .resolve_char(fc_cache, ch)
7229 .map(|(id, _)| id)
7230 .or_else(|| {
7238 loaded_fonts
7239 .iter()
7240 .filter(|(_, font)| font.has_glyph(ch as u32))
7241 .map(|(id, _)| *id)
7242 .min()
7243 })
7244 .or(notdef_font_id);
7248 if let Some(font_id) = font_id {
7249 match segments.last_mut() {
7250 Some(last) if last.2 == font_id && last.1 == byte_idx => {
7251 last.1 = char_end;
7253 }
7254 _ => {
7255 segments.push((byte_idx, char_end, font_id));
7257 }
7258 }
7259 }
7260 }
7261
7262 segments
7263}
7264
7265fn measure_run_advance<T: ParsedFontTrait>(
7272 text: &str,
7273 style: &Arc<StyleProperties>,
7274 script: Script,
7275 source: ContentIndex,
7276 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
7277 fc_cache: &FcFontCache,
7278 loaded_fonts: &LoadedFonts<T>,
7279) -> Option<f32> {
7280 if text.is_empty() {
7281 return Some(0.0);
7282 }
7283 let language = script_to_language(script, text);
7284 match &style.font_stack {
7285 FontStack::Ref(font_ref) => {
7286 let glyphs = font_ref
7287 .shape_text(text, script, language, BidiDirection::Ltr, style.as_ref())
7288 .ok()?;
7289 Some(glyphs.iter().map(|g| g.advance + g.kerning).sum())
7290 }
7291 FontStack::Stack(selectors) => {
7292 let cache_key = FontChainKey::from_selectors(selectors);
7293 let font_chain = font_chain_cache.get(&cache_key)?;
7294 let clusters = shape_with_font_fallback(
7295 text, script, language, BidiDirection::Ltr, style, source, None, font_chain,
7296 fc_cache, loaded_fonts,
7297 )
7298 .ok()?;
7299 Some(clusters.iter().map(|c| c.advance).sum())
7300 }
7301 }
7302}
7303
7304#[allow(clippy::cast_possible_truncation)] fn shape_with_font_fallback<T: ParsedFontTrait>(
7311 text: &str,
7312 script: Script,
7313 language: Language,
7314 direction: BidiDirection,
7315 style: &Arc<StyleProperties>,
7316 source_index: ContentIndex,
7317 source_node_id: Option<NodeId>,
7318 font_chain: &rust_fontconfig::FontFallbackChain,
7319 fc_cache: &FcFontCache,
7320 loaded_fonts: &LoadedFonts<T>,
7321) -> Result<Vec<ShapedCluster>, LayoutError> {
7322 static FONT_FB_DEBUG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7330 let dbg = *FONT_FB_DEBUG.get_or_init(|| {
7331 std::env::var_os("AZ_FONT_FALLBACK_DEBUG").is_some()
7332 });
7333
7334 let segments = split_text_by_font_coverage(text, font_chain, fc_cache, loaded_fonts);
7335
7336 if dbg && segments.len() > 1 {
7337 eprintln!(
7338 "[FONT FALLBACK] text needs {} font segments for '{}' ({}..{} bytes)",
7339 segments.len(),
7340 text.chars().take(40).collect::<String>(),
7341 0, text.len()
7342 );
7343 }
7344
7345 unsafe { crate::az_mark(0x60850_u32, segments.len() as u32); } if segments.len() <= 1 {
7347 let (seg_start, seg_end, font_id) = if let Some(s) = segments.first() { unsafe { crate::az_mark(0x60854_u32, 0x0000_0001_u32); } s } else {
7349 unsafe { crate::az_mark(0x60854_u32, 0x0000_00EE_u32); } if dbg {
7351 eprintln!("[FONT FALLBACK] no font could render any char in '{}'", text.chars().take(20).collect::<String>());
7352 }
7353 return Ok(Vec::new());
7354 };
7355 let font = if let Some(f) = loaded_fonts.get(font_id) { unsafe { crate::az_mark(0x60858_u32, 0x0000_0001_u32); } f } else {
7356 unsafe { crate::az_mark(0x60858_u32, 0x0000_00EE_u32); } if dbg {
7358 eprintln!("[FONT FALLBACK] font {:?} not in loaded_fonts for '{}'", font_id, text.chars().take(20).collect::<String>());
7359 }
7360 return Ok(Vec::new());
7361 };
7362 if *seg_start == 0 && *seg_end == text.len() {
7364 unsafe { crate::az_mark(0x60860_u32, 0xC0DE_0860_u32); } return shape_text_correctly(
7366 text, script, language, direction,
7367 font, style, source_index, source_node_id,
7368 );
7369 }
7370 let mut clusters = shape_text_correctly(
7371 &text[*seg_start..*seg_end], script, language, direction,
7372 font, style, source_index, source_node_id,
7373 )?;
7374 if *seg_start > 0 {
7375 for cluster in &mut clusters {
7376 cluster.source_cluster_id.start_byte_in_run += *seg_start as u32;
7377 }
7378 }
7379 return Ok(clusters);
7380 }
7381
7382 let mut all_clusters = Vec::new();
7384 for (seg_start, seg_end, font_id) in &segments {
7385 let Some(font) = loaded_fonts.get(font_id) else {
7386 if dbg {
7387 eprintln!("[FONT FALLBACK] font {font_id:?} NOT loaded, skipping segment bytes {seg_start}..{seg_end}");
7388 }
7389 continue;
7390 };
7391 let segment_text = &text[*seg_start..*seg_end];
7392 if dbg {
7393 eprintln!(
7394 "[FONT FALLBACK] text='{segment_text}' uses font {font_id:?} (bytes {seg_start}..{seg_end})"
7395 );
7396 }
7397 let mut seg_clusters = shape_text_correctly(
7398 segment_text, script, language, direction,
7399 font, style, source_index, source_node_id,
7400 )?;
7401 if *seg_start > 0 {
7404 for cluster in &mut seg_clusters {
7405 cluster.source_cluster_id.start_byte_in_run += *seg_start as u32;
7406 }
7407 }
7408 all_clusters.extend(seg_clusters);
7409 }
7410 Ok(all_clusters)
7411}
7412
7413#[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>(
7421 visual_items: &[VisualItem],
7422 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
7423 fc_cache: &FcFontCache,
7424 loaded_fonts: &LoadedFonts<T>,
7425 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
7426) -> Result<Vec<ShapedItem>, LayoutError> {
7427 let mut shaped = Vec::new();
7428 let mut idx = 0;
7429 let mut _coalesced_runs = 0usize;
7430 let mut _total_runs = 0usize;
7431 let mut _shape_calls = 0usize;
7432
7433 while idx < visual_items.len() {
7436 let item = &visual_items[idx];
7437 match &item.logical_source {
7438 LogicalItem::Text {
7439 style,
7440 source,
7441 marker_position_outside,
7442 source_node_id,
7443 ..
7444 } => {
7445 let layout_hash = style.layout_hash();
7446 let bidi_level = item.bidi_level;
7447 let script = item.script;
7448
7449 let mut coalesce_end = idx + 1;
7455 while coalesce_end < visual_items.len() {
7456 let next = &visual_items[coalesce_end];
7457 if let LogicalItem::Text { style: next_style, .. } = &next.logical_source {
7458 if next_style.layout_hash() == layout_hash
7459 && next.bidi_level == bidi_level
7460 && next.script == script
7461 {
7462 coalesce_end += 1;
7463 } else {
7464 break;
7465 }
7466 } else {
7467 break;
7468 }
7469 }
7470
7471 let coalesce_count = coalesce_end - idx;
7472
7473 if coalesce_count > 1 {
7474 _coalesced_runs += coalesce_count;
7475 _shape_calls += 1;
7476 let total_text_len: usize = visual_items[idx..coalesce_end]
7482 .iter()
7483 .map(|v| v.text.len())
7484 .sum();
7485 let mut merged_text = String::with_capacity(total_text_len);
7486 let mut byte_ranges: Vec<(
7488 usize, usize,
7489 Arc<StyleProperties>,
7490 ContentIndex,
7491 Option<NodeId>,
7492 Option<bool>,
7493 usize,
7494 )> = Vec::with_capacity(coalesce_count);
7495
7496 for item in &visual_items[idx..coalesce_end] {
7497 let start = merged_text.len();
7498 merged_text.push_str(&item.text);
7499 let end = merged_text.len();
7500 if let LogicalItem::Text {
7501 style: s, source: src, source_node_id: nid,
7502 marker_position_outside: mpo, ..
7503 } = &item.logical_source {
7504 byte_ranges.push((start, end, s.clone(), *src, *nid, *mpo, item.run_byte_offset));
7505 }
7506 }
7507
7508 if let Some(msgs) = debug_messages {
7509 msgs.push(LayoutDebugMessage::info(format!(
7510 "[TextLayout] Coalescing {} text runs ({} bytes) into single shaping call",
7511 coalesce_count, merged_text.len()
7512 )));
7513 }
7514
7515 let direction = if bidi_level.is_rtl() {
7516 BidiDirection::Rtl
7517 } else {
7518 BidiDirection::Ltr
7519 };
7520 let language = script_to_language(script, &merged_text);
7521
7522 let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
7525 FontStack::Ref(font_ref) => {
7526 shape_text_correctly(
7527 &merged_text, script, language, direction,
7528 font_ref, style, *source, *source_node_id,
7529 )
7530 }
7531 FontStack::Stack(selectors) => {
7532 let cache_key = FontChainKey::from_selectors(selectors);
7533 let Some(font_chain) = font_chain_cache.get(&cache_key) else { idx = coalesce_end; continue; };
7534 shape_with_font_fallback(
7536 &merged_text, script, language, direction,
7537 style, *source, *source_node_id,
7538 font_chain, fc_cache, loaded_fonts,
7539 )
7540 }
7541 };
7542
7543 let shaped_clusters = shaped_clusters_result?;
7544
7545 for cluster in shaped_clusters {
7550 let byte_pos = cluster.source_cluster_id.start_byte_in_run as usize;
7551 let orig = byte_ranges.iter().find(|(start, end, ..)| {
7553 byte_pos >= *start && byte_pos < *end
7554 });
7555 let mut cluster = cluster;
7556 if let Some((range_start, _, orig_style, orig_source, orig_nid, orig_mpo, orig_run_offset)) = orig {
7557 cluster.style = orig_style.clone();
7559 cluster.source_content_index = *orig_source;
7560 cluster.source_node_id = *orig_nid;
7561 cluster.source_cluster_id.source_run = orig_source.run_index;
7565 cluster.source_cluster_id.start_byte_in_run = (byte_pos - range_start + *orig_run_offset) as u32;
7566 for glyph in &mut cluster.glyphs {
7568 glyph.style = orig_style.clone();
7569 }
7570 if let Some(is_outside) = orig_mpo {
7571 cluster.marker_position_outside = Some(*is_outside);
7572 }
7573 }
7574 shaped.push(ShapedItem::Cluster(cluster));
7575 }
7576
7577 idx = coalesce_end;
7578 continue;
7579 }
7580
7581 _total_runs += 1;
7583 _shape_calls += 1;
7584 let direction = if item.bidi_level.is_rtl() {
7585 BidiDirection::Rtl
7586 } else {
7587 BidiDirection::Ltr
7588 };
7589
7590 let language = script_to_language(item.script, &item.text);
7591
7592 let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
7594 FontStack::Ref(font_ref) => {
7595 unsafe { crate::az_mark(0x60820_u32, 0x0000_0001_u32); } if let Some(msgs) = debug_messages {
7598 msgs.push(LayoutDebugMessage::info(format!(
7599 "[TextLayout] Using direct FontRef for text: '{}'",
7600 item.text.chars().take(30).collect::<String>()
7601 )));
7602 }
7603 shape_text_correctly(
7604 &item.text,
7605 item.script,
7606 language,
7607 direction,
7608 font_ref,
7609 style,
7610 *source,
7611 *source_node_id,
7612 )
7613 }
7614 FontStack::Stack(selectors) => {
7615 unsafe { crate::az_mark(0x60820_u32, 0x0000_0002_u32); } let cache_key = FontChainKey::from_selectors(selectors);
7618 unsafe { crate::az_mark(0x60824_u32, font_chain_cache.len() as u32); } let Some(font_chain) = font_chain_cache.get(&cache_key) else {
7626 if let Some(msgs) = debug_messages {
7627 msgs.push(LayoutDebugMessage::warning(format!(
7628 "[TextLayout] Font chain not pre-resolved for {:?} - text will \
7629 not be rendered",
7630 cache_key.font_families
7631 )));
7632 }
7633 idx += 1;
7634 continue;
7635 };
7636
7637 shape_with_font_fallback(
7639 &item.text, item.script, language, direction,
7640 style, *source, *source_node_id,
7641 font_chain, fc_cache, loaded_fonts,
7642 )
7643 }
7644 };
7645
7646 let mut shaped_clusters = shaped_clusters_result?;
7647
7648 let run_byte_offset = item.run_byte_offset as u32;
7653 if run_byte_offset != 0 {
7654 for cluster in &mut shaped_clusters {
7655 cluster.source_cluster_id.start_byte_in_run = cluster
7656 .source_cluster_id
7657 .start_byte_in_run
7658 .saturating_add(run_byte_offset);
7659 }
7660 }
7661
7662 if let Some(is_outside) = marker_position_outside {
7664 for cluster in &mut shaped_clusters {
7665 cluster.marker_position_outside = Some(*is_outside);
7666 }
7667 }
7668
7669 shaped.extend(shaped_clusters.into_iter().map(ShapedItem::Cluster));
7670 }
7671 LogicalItem::Tab { source, style } => {
7678 if style.tab_size == 0.0 {
7679 shaped.push(ShapedItem::Tab {
7681 source: *source,
7682 bounds: Rect {
7683 x: 0.0,
7684 y: 0.0,
7685 width: 0.0,
7686 height: 0.0,
7687 },
7688 });
7689 } else {
7690 let space_advance_approx = style.font_size_px * SPACE_WIDTH_RATIO;
7694 let ls = style.letter_spacing.resolve_px(style.font_size_px);
7696 let ws = style.word_spacing.resolve_px(style.font_size_px);
7697 let tab_interval = style.tab_size * (space_advance_approx + ls + ws);
7699 let current_advance: f32 = shaped.iter().map(|item| {
7701 match item {
7702 ShapedItem::Cluster(c) => c.advance,
7703 ShapedItem::Tab { bounds, .. } => bounds.width,
7704 ShapedItem::Object { bounds, .. } => bounds.width,
7705 _ => 0.0,
7706 }
7707 }).sum();
7708 let next_tab_stop = ((current_advance / tab_interval).floor() + 1.0) * tab_interval;
7710 let mut tab_width = next_tab_stop - current_advance;
7711 let half_ch = space_advance_approx * 0.5;
7713 if tab_width < half_ch {
7714 tab_width += tab_interval;
7715 }
7716 shaped.push(ShapedItem::Tab {
7717 source: *source,
7718 bounds: Rect {
7719 x: 0.0,
7720 y: 0.0,
7721 width: tab_width,
7722 height: 0.0,
7723 },
7724 });
7725 }
7726 }
7727 LogicalItem::Ruby {
7728 source,
7729 base_text,
7730 ruby_text,
7731 style,
7732 } => {
7733 let base_font_size = style.font_size_px;
7742 let annotation_font_size = base_font_size * RUBY_ANNOTATION_FONT_SCALE;
7743
7744 let mut annotation_props = (**style).clone();
7745 annotation_props.font_size_px = annotation_font_size;
7746 let annotation_style = Arc::new(annotation_props);
7747
7748 let base_width = measure_run_advance(
7751 base_text, style, item.script, *source, font_chain_cache, fc_cache,
7752 loaded_fonts,
7753 )
7754 .unwrap_or_else(|| base_text.chars().count() as f32 * base_font_size);
7755 let annotation_width = measure_run_advance(
7756 ruby_text, &annotation_style, item.script, *source, font_chain_cache,
7757 fc_cache, loaded_fonts,
7758 )
7759 .unwrap_or_else(|| ruby_text.chars().count() as f32 * annotation_font_size);
7760
7761 let base_line_height =
7762 style.line_height.resolve(base_font_size, 0.0, 0.0, 0.0, 0);
7763 let annotation_line_height = annotation_style.line_height.resolve(
7764 annotation_font_size, 0.0, 0.0, 0.0, 0,
7765 );
7766 let (reserved_width, reserved_height) = ruby_reserved_box(
7769 base_width,
7770 annotation_width,
7771 base_line_height,
7772 annotation_line_height,
7773 );
7774
7775 shaped.push(ShapedItem::Object {
7781 source: *source,
7782 bounds: Rect {
7783 x: 0.0,
7784 y: 0.0,
7785 width: reserved_width,
7786 height: reserved_height,
7787 },
7788 baseline_offset: 0.0,
7789 content: InlineContent::Text(StyledRun {
7790 text: base_text.clone(),
7791 style: style.clone(),
7792 logical_start_byte: 0,
7793 source_node_id: None,
7794 }),
7795 });
7796 }
7797 LogicalItem::CombinedText {
7798 style,
7799 source,
7800 text,
7801 } => {
7802 let language = script_to_language(item.script, &item.text);
7803
7804 let text = if text.chars().count() > 1 {
7810 let converted: String = text.chars().map(|c| {
7811 let cp = c as u32;
7812 if (0xFF01..=0xFF5E).contains(&cp) {
7813 char::from_u32(cp - 0xFF01 + 0x0021).unwrap_or(c)
7815 } else {
7816 c
7817 }
7818 }).collect();
7819 converted
7820 } else {
7821 text.clone()
7822 };
7823
7824 let glyphs: Vec<Glyph> = match &style.font_stack {
7829 FontStack::Ref(font_ref) => {
7830 if let Some(msgs) = debug_messages {
7832 msgs.push(LayoutDebugMessage::info(format!(
7833 "[TextLayout] Using direct FontRef for CombinedText: '{}'",
7834 text.chars().take(30).collect::<String>()
7835 )));
7836 }
7837 font_ref.shape_text(
7838 &text,
7839 item.script,
7840 language,
7841 BidiDirection::Ltr,
7842 style.as_ref(),
7843 )?
7844 }
7845 FontStack::Stack(selectors) => {
7846 let cache_key = FontChainKey::from_selectors(selectors);
7848
7849 let Some(font_chain) = font_chain_cache.get(&cache_key) else {
7850 if let Some(msgs) = debug_messages {
7851 msgs.push(LayoutDebugMessage::warning(format!(
7852 "[TextLayout] Font chain not pre-resolved for CombinedText {:?}",
7853 cache_key.font_families
7854 )));
7855 }
7856 idx += 1;
7857 continue;
7858 };
7859
7860 let segments = split_text_by_font_coverage(&text, font_chain, fc_cache, loaded_fonts);
7862 let mut all_glyphs = Vec::new();
7863 for (seg_start, seg_end, font_id) in &segments {
7864 let Some(font) = loaded_fonts.get(font_id) else { continue; };
7865 let segment_text = &text[*seg_start..*seg_end];
7866 let mut seg_glyphs = font.shape_text(
7867 segment_text,
7868 item.script,
7869 language,
7870 BidiDirection::Ltr,
7871 style.as_ref(),
7872 )?;
7873 if *seg_start > 0 {
7875 for g in &mut seg_glyphs {
7876 g.logical_byte_index += *seg_start;
7877 g.cluster += *seg_start as u32;
7878 }
7879 }
7880 all_glyphs.extend(seg_glyphs);
7881 }
7882 if all_glyphs.is_empty() {
7883 idx += 1;
7884 continue;
7885 }
7886 all_glyphs
7887 }
7888 };
7889
7890 let shaped_glyphs: ShapedGlyphVec = glyphs
7891 .into_iter()
7892 .map(|g| ShapedGlyph {
7893 kind: GlyphKind::Character,
7894 glyph_id: g.glyph_id,
7895 script: g.script,
7896 font_hash: g.font_hash,
7897 font_metrics: g.font_metrics,
7898 style: g.style,
7899 cluster_offset: 0,
7900 advance: g.advance,
7901 kerning: g.kerning,
7902 offset: g.offset,
7903 vertical_advance: g.vertical_advance,
7904 vertical_offset: g.vertical_bearing,
7905 })
7906 .collect();
7907
7908 let total_width: f32 = shaped_glyphs.iter().map(|g| g.advance + g.kerning).sum();
7910 let em_size = shaped_glyphs.first()
7916 .map_or(style.font_size_px, |g| g.style.font_size_px);
7917 let bounds = Rect {
7918 x: 0.0,
7919 y: 0.0,
7920 width: total_width,
7921 height: em_size,
7922 };
7923
7924 shaped.push(ShapedItem::CombinedBlock {
7925 source: *source,
7926 glyphs: shaped_glyphs,
7927 bounds,
7928 baseline_offset: em_size / 2.0,
7929 });
7930 }
7931 LogicalItem::Object {
7932 content, source, ..
7933 } => {
7934 let (bounds, baseline) = measure_inline_object(content)?;
7935 shaped.push(ShapedItem::Object {
7936 source: *source,
7937 bounds,
7938 baseline_offset: baseline,
7939 content: content.clone(),
7940 });
7941 }
7942 LogicalItem::Break { source, break_info } => {
7943 shaped.push(ShapedItem::Break {
7944 source: *source,
7945 break_info: *break_info,
7946 });
7947 }
7948 }
7949 idx += 1;
7950 }
7951
7952 Ok(shaped)
7953}
7954
7955const fn is_hanging_punctuation_char(c: char) -> bool {
7958 matches!(c,
7959 ',' | '.' | '\u{060C}' | '\u{06D4}' | '\u{3001}' | '\u{3002}' | '\u{FF0C}' | '\u{FF0E}' | '\u{FE50}' | '\u{FE51}' | '\u{FE52}' | '\u{FF61}' | '\u{FF64}' )
7973}
7974
7975fn is_hanging_punctuation(item: &ShapedItem) -> bool {
7980 if let ShapedItem::Cluster(c) = item {
7981 if c.glyphs.len() == 1 {
7982 c.text.chars().next().is_some_and(is_hanging_punctuation_char)
7983 } else {
7984 false
7985 }
7986 } else {
7987 false
7988 }
7989}
7990
7991#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] fn shape_text_correctly<T: ParsedFontTrait>(
7994 text: &str,
7995 script: Script,
7996 language: Language,
7997 direction: BidiDirection,
7998 font: &T, style: &Arc<StyleProperties>,
8000 source_index: ContentIndex,
8001 source_node_id: Option<NodeId>,
8002) -> Result<Vec<ShapedCluster>, LayoutError> {
8003 unsafe { crate::az_mark(0x60864_u32, 0xC0DE_0864_u32); } let glyphs = font.shape_text(text, script, language, direction, style.as_ref())?;
8005 unsafe { crate::az_mark(0x60868_u32, (glyphs.len() as u32) | 0x8000_0000_u32); } if glyphs.is_empty() {
8008 return Ok(Vec::new());
8009 }
8010
8011 let mut clusters = Vec::new();
8012
8013 let mut current_cluster_glyphs = Vec::new();
8015 let mut cluster_id = glyphs[0].cluster;
8016 let mut cluster_start_byte_in_text = glyphs[0].logical_byte_index;
8017
8018 for glyph in glyphs {
8019 if glyph.cluster != cluster_id {
8020 let advance = current_cluster_glyphs
8022 .iter()
8023 .map(|g: &Glyph| g.advance)
8024 .sum();
8025
8026 let (start, end) = if cluster_start_byte_in_text <= glyph.logical_byte_index {
8029 (cluster_start_byte_in_text, glyph.logical_byte_index)
8030 } else {
8031 (glyph.logical_byte_index, cluster_start_byte_in_text)
8032 };
8033 let cluster_text = text.get(start..end).unwrap_or("");
8034
8035 clusters.push(ShapedCluster {
8036 text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
8038 source_run: source_index.run_index,
8039 start_byte_in_run: cluster_id,
8040 },
8041 source_content_index: source_index,
8042 source_node_id,
8043 glyphs: current_cluster_glyphs
8044 .iter()
8045 .map(|g| {
8046 let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
8048 (g.logical_byte_index - cluster_start_byte_in_text) as u32
8049 } else {
8050 0
8051 };
8052 ShapedGlyph {
8053 kind: if g.glyph_id == 0 {
8054 GlyphKind::NotDef
8055 } else {
8056 GlyphKind::Character
8057 },
8058 glyph_id: g.glyph_id,
8059 script: g.script,
8060 font_hash: g.font_hash,
8061 font_metrics: g.font_metrics,
8062 style: g.style.clone(),
8063 cluster_offset,
8064 advance: g.advance,
8065 kerning: g.kerning,
8066 vertical_advance: g.vertical_advance,
8067 vertical_offset: g.vertical_bearing,
8068 offset: g.offset,
8069 }
8070 })
8071 .collect(),
8072 advance,
8073 direction,
8074 style: style.clone(),
8075 marker_position_outside: None,
8076 is_first_fragment: true,
8077 is_last_fragment: true,
8078 });
8079 current_cluster_glyphs.clear();
8080 cluster_id = glyph.cluster;
8081 cluster_start_byte_in_text = glyph.logical_byte_index;
8082 }
8083 current_cluster_glyphs.push(glyph);
8084 }
8085
8086 if !current_cluster_glyphs.is_empty() {
8088 let advance = current_cluster_glyphs
8089 .iter()
8090 .map(|g: &Glyph| g.advance)
8091 .sum();
8092 let cluster_text = text.get(cluster_start_byte_in_text..).unwrap_or("");
8093 clusters.push(ShapedCluster {
8094 text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
8096 source_run: source_index.run_index,
8097 start_byte_in_run: cluster_id,
8098 },
8099 source_content_index: source_index,
8100 source_node_id,
8101 glyphs: current_cluster_glyphs
8102 .iter()
8103 .map(|g| {
8104 let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
8106 (g.logical_byte_index - cluster_start_byte_in_text) as u32
8107 } else {
8108 0
8109 };
8110 ShapedGlyph {
8111 kind: if g.glyph_id == 0 {
8112 GlyphKind::NotDef
8113 } else {
8114 GlyphKind::Character
8115 },
8116 glyph_id: g.glyph_id,
8117 font_hash: g.font_hash,
8118 font_metrics: g.font_metrics,
8119 style: g.style.clone(),
8120 script: g.script,
8121 vertical_advance: g.vertical_advance,
8122 vertical_offset: g.vertical_bearing,
8123 cluster_offset,
8124 advance: g.advance,
8125 kerning: g.kerning,
8126 offset: g.offset,
8127 }
8128 })
8129 .collect(),
8130 advance,
8131 direction,
8132 style: style.clone(),
8133 marker_position_outside: None,
8134 is_first_fragment: true,
8135 is_last_fragment: true,
8136 });
8137 }
8138
8139 Ok(clusters)
8140}
8141
8142fn measure_inline_object(item: &InlineContent) -> Result<(Rect, f32), LayoutError> {
8144 match item {
8145 InlineContent::Image(img) => {
8146 let size = img.display_size.unwrap_or(img.intrinsic_size);
8147 Ok((
8148 Rect {
8149 x: 0.0,
8150 y: 0.0,
8151 width: size.width,
8152 height: size.height,
8153 },
8154 img.baseline_offset,
8155 ))
8156 }
8157 InlineContent::Shape(shape) => Ok({
8158 let size = shape.shape_def.get_size();
8159 (
8160 Rect {
8161 x: 0.0,
8162 y: 0.0,
8163 width: size.width,
8164 height: size.height,
8165 },
8166 shape.baseline_offset,
8167 )
8168 }),
8169 InlineContent::Space(space) => Ok((
8170 Rect {
8171 x: 0.0,
8172 y: 0.0,
8173 width: space.width,
8174 height: 0.0,
8175 },
8176 0.0,
8177 )),
8178 InlineContent::Marker { .. } => {
8179 Err(LayoutError::InvalidText(
8181 "Marker is text content, not a measurable object".into(),
8182 ))
8183 }
8184 _ => Err(LayoutError::InvalidText("Not a measurable object".into())),
8185 }
8186}
8187
8188fn apply_text_orientation(
8194 items: Arc<Vec<ShapedItem>>,
8195 constraints: &UnifiedConstraints,
8196) -> Arc<Vec<ShapedItem>> {
8197 if !constraints.is_vertical() {
8198 return items;
8199 }
8200
8201 let mut oriented_items = Vec::with_capacity(items.len());
8202 let writing_mode = constraints.writing_mode.unwrap_or_default();
8203
8204 for item in items.iter() {
8205 match item {
8206 ShapedItem::Cluster(cluster) => {
8207 let mut new_cluster = cluster.clone();
8208 let mut total_vertical_advance = 0.0;
8209
8210 for glyph in &mut new_cluster.glyphs {
8211 if glyph.vertical_advance > 0.0 {
8214 total_vertical_advance += glyph.vertical_advance;
8215 } else {
8216 let fallback_advance = cluster.style.line_height.resolve_with_metrics(cluster.style.font_size_px, &glyph.font_metrics);
8218 glyph.vertical_advance = fallback_advance;
8219 glyph.vertical_offset = Point {
8221 x: -glyph.advance / 2.0,
8222 y: 0.0,
8223 };
8224 total_vertical_advance += fallback_advance;
8225 }
8226 }
8227 new_cluster.advance = total_vertical_advance;
8229 oriented_items.push(ShapedItem::Cluster(new_cluster));
8230 }
8231 ShapedItem::Object {
8233 source,
8234 bounds,
8235 baseline_offset,
8236 content,
8237 } => {
8238 let mut new_bounds = *bounds;
8239 std::mem::swap(&mut new_bounds.width, &mut new_bounds.height);
8240 oriented_items.push(ShapedItem::Object {
8241 source: *source,
8242 bounds: new_bounds,
8243 baseline_offset: *baseline_offset,
8244 content: content.clone(),
8245 });
8246 }
8247 _ => oriented_items.push(item.clone()),
8248 }
8249 }
8250
8251 Arc::new(oriented_items)
8252}
8253
8254fn get_item_vertical_align(item: &ShapedItem) -> Option<VerticalAlign> {
8263 match item {
8264 ShapedItem::Object { content, .. } => match content {
8265 InlineContent::Image(img) => Some(img.alignment),
8266 InlineContent::Shape(shape) => Some(shape.alignment),
8267 _ => None,
8268 },
8269 ShapedItem::Cluster(c) => match c.style.vertical_align {
8274 VerticalAlign::Baseline => None,
8275 va => Some(va),
8276 },
8277 _ => None,
8278 }
8279}
8280
8281#[allow(clippy::match_same_arms)] #[must_use] pub fn get_item_vertical_metrics_approx(item: &ShapedItem) -> (f32, f32) {
8285 if let ShapedItem::Cluster(c) = item {
8287 if !c.glyphs.is_empty() {
8288 let (asc, desc) = c.glyphs
8290 .iter()
8291 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
8292 let metrics = &glyph.font_metrics;
8293 if metrics.units_per_em == 0 {
8294 return (max_asc, max_desc);
8295 }
8296 let scale = glyph.style.font_size_px / f32::from(metrics.units_per_em);
8297 let font_ascent = metrics.ascent * scale;
8298 let font_descent = (-metrics.descent * scale).max(0.0);
8299 let ad = font_ascent + font_descent;
8300 let resolved_lh = c.style.line_height.resolve_with_metrics(glyph.style.font_size_px, &glyph.font_metrics);
8301 let half_leading = (resolved_lh - ad) / 2.0;
8302 (max_asc.max(font_ascent + half_leading), max_desc.max(font_descent + half_leading))
8303 });
8304 return (asc, desc);
8305 }
8306 }
8307 match item {
8309 ShapedItem::Cluster(c) => {
8310 let lh = c.style.line_height.resolve(c.style.font_size_px, 0.0, 0.0, 0.0, 0);
8311 (lh * FALLBACK_ASCENT_RATIO, lh * FALLBACK_DESCENT_RATIO)
8312 }
8313 ShapedItem::CombinedBlock { bounds, .. } => {
8314 (bounds.height * FALLBACK_ASCENT_RATIO, bounds.height * FALLBACK_DESCENT_RATIO)
8315 }
8316 ShapedItem::Object { bounds, .. } => (bounds.height, 0.0),
8317 ShapedItem::Tab { bounds, .. } => {
8318 (bounds.height * FALLBACK_ASCENT_RATIO, bounds.height * FALLBACK_DESCENT_RATIO)
8319 }
8320 ShapedItem::Break { .. } => (0.0, 0.0),
8321 }
8322}
8323
8324#[must_use] pub fn get_item_vertical_metrics(item: &ShapedItem, constraints: &UnifiedConstraints) -> (f32, f32) {
8338 match item {
8340 ShapedItem::Cluster(c) => {
8341 if c.glyphs.is_empty() {
8342 let ad = constraints.strut_ascent + constraints.strut_descent;
8348 let resolved_lh = c.style.line_height.resolve(c.style.font_size_px, 0.0, 0.0, 0.0, 0);
8349 let half_leading = (resolved_lh - ad) / 2.0;
8350 return (constraints.strut_ascent + half_leading, constraints.strut_descent + half_leading);
8351 }
8352 c.glyphs
8365 .iter()
8366 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
8367 let metrics = &glyph.font_metrics;
8368 if metrics.units_per_em == 0 {
8369 return (max_asc, max_desc);
8370 }
8371 let scale = glyph.style.font_size_px / f32::from(metrics.units_per_em);
8372 let a = metrics.ascent * scale;
8373 let d = (-metrics.descent * scale).max(0.0);
8376 let ad = a + d;
8377 let resolved_lh = glyph.style.line_height.resolve_with_metrics(glyph.style.font_size_px, &glyph.font_metrics);
8378 let leading = resolved_lh - ad;
8379 let half_leading = leading / 2.0;
8380 let item_asc = a + half_leading;
8381 let item_desc = d + half_leading;
8382 (max_asc.max(item_asc), max_desc.max(item_desc))
8383 })
8384 }
8385 ShapedItem::Object {
8386 bounds,
8387 baseline_offset,
8388 ..
8389 } => {
8390 let ascent = bounds.height - *baseline_offset;
8393 let descent = *baseline_offset;
8394 (ascent.max(0.0), descent.max(0.0))
8395 }
8396 ShapedItem::CombinedBlock {
8397 bounds,
8398 baseline_offset,
8399 ..
8400 } => {
8401 let ascent = bounds.height - *baseline_offset;
8403 let descent = *baseline_offset;
8404 (ascent.max(0.0), descent.max(0.0))
8405 }
8406 _ => (0.0, 0.0), }
8408}
8409
8410fn calculate_line_metrics(
8424 items: &[ShapedItem],
8425 default_vertical_align: VerticalAlign,
8426 constraints: &UnifiedConstraints,
8427) -> (f32, f32) {
8428 let (mut max_asc, mut max_desc) = items
8432 .iter()
8433 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), item| {
8434 let effective_align = get_item_vertical_align(item)
8435 .unwrap_or(default_vertical_align);
8436 match effective_align {
8437 VerticalAlign::Top | VerticalAlign::Bottom => {
8438 (max_asc, max_desc)
8440 }
8441 _ => {
8442 let (item_asc, item_desc) = get_item_vertical_metrics(item, constraints);
8443 (max_asc.max(item_asc), max_desc.max(item_desc))
8444 }
8445 }
8446 });
8447
8448 let baseline_line_height = max_asc + max_desc;
8449
8450 for item in items {
8453 let effective_align = get_item_vertical_align(item)
8454 .unwrap_or(default_vertical_align);
8455 match effective_align {
8456 VerticalAlign::Top | VerticalAlign::Bottom => {
8457 let (item_asc, item_desc) = get_item_vertical_metrics(item, constraints);
8458 let item_height = item_asc + item_desc;
8459 if item_height > baseline_line_height {
8460 if effective_align == VerticalAlign::Top {
8462 max_desc = max_desc.max(item_height - max_asc);
8464 } else {
8465 max_asc = max_asc.max(item_height - max_desc);
8467 }
8468 }
8469 }
8470 _ => {} }
8472 }
8473
8474 (max_asc, max_desc)
8475}
8476
8477fn apply_l2_visual_reversal(line_items: &mut [ShapedItem]) {
8494 let dir_of = |it: &ShapedItem| it.as_cluster().map(|c| c.direction);
8495 let mut i = 0;
8496 while i < line_items.len() {
8497 let Some(dir) = dir_of(&line_items[i]) else {
8498 i += 1;
8499 continue;
8500 };
8501 let mut j = i + 1;
8502 while j < line_items.len() && dir_of(&line_items[j]) == Some(dir) {
8503 j += 1;
8504 }
8505 if dir == BidiDirection::Rtl {
8506 line_items[i..j].reverse();
8507 }
8508 i = j;
8509 }
8510}
8511
8512#[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn perform_fragment_layout<T: ParsedFontTrait>(
8554 cursor: &mut BreakCursor<'_>,
8555 logical_items: &[LogicalItem],
8556 fragment_constraints: &UnifiedConstraints,
8557 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
8558 fonts: &LoadedFonts<T>,
8559) -> Result<UnifiedLayout, LayoutError> {
8560 const MAX_EMPTY_SEGMENTS: usize = 1000; if let Some(msgs) = debug_messages {
8562 msgs.push(LayoutDebugMessage::info(
8563 "\n--- Entering perform_fragment_layout ---".to_string(),
8564 ));
8565 msgs.push(LayoutDebugMessage::info(format!(
8566 "Constraints: available_width={:?}, available_height={:?}, columns={}, text_wrap={:?}",
8567 fragment_constraints.available_width,
8568 fragment_constraints.available_height,
8569 fragment_constraints.columns,
8570 fragment_constraints.text_wrap
8571 )));
8572 }
8573
8574 if fragment_constraints.text_wrap == TextWrap::Balance {
8577 if let Some(msgs) = debug_messages {
8578 msgs.push(LayoutDebugMessage::info(
8579 "Using Knuth-Plass algorithm for text-wrap: balance".to_string(),
8580 ));
8581 }
8582
8583 let shaped_items: Vec<ShapedItem> = cursor.drain_remaining();
8585
8586 let hyphenator = if fragment_constraints.hyphenation == Hyphens::Auto {
8588 fragment_constraints
8589 .hyphenation_language
8590 .and_then(|lang| get_hyphenator(lang).ok())
8591 } else {
8592 None
8593 };
8594
8595 return Ok(crate::text3::knuth_plass::kp_layout(
8597 &shaped_items,
8598 logical_items,
8599 fragment_constraints,
8600 hyphenator.as_ref(),
8601 fonts,
8602 ));
8603 }
8604
8605 let hyphenator = if fragment_constraints.hyphenation == Hyphens::Auto {
8607 fragment_constraints
8608 .hyphenation_language
8609 .and_then(|lang| get_hyphenator(lang).ok())
8610 } else {
8611 None
8612 };
8613
8614 let mut positioned_items = Vec::new();
8615 let mut layout_bounds = Rect::default();
8616
8617 let num_columns = fragment_constraints.columns.max(1);
8618 let total_column_gap = fragment_constraints.column_gap * (num_columns - 1) as f32;
8619
8620 let is_min_content = matches!(fragment_constraints.available_width, AvailableSpace::MinContent);
8633 let is_max_content = matches!(fragment_constraints.available_width, AvailableSpace::MaxContent);
8634
8635 let column_width = match fragment_constraints.available_width {
8636 AvailableSpace::Definite(width) => (width - total_column_gap) / num_columns as f32,
8637 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
8638 f32::MAX / 2.0
8641 }
8642 };
8643 let mut current_column = 0;
8644 if let Some(msgs) = debug_messages {
8645 msgs.push(LayoutDebugMessage::info(format!(
8646 "Column width calculated: {column_width}"
8647 )));
8648 }
8649
8650 let base_direction = if fragment_constraints.unicode_bidi == UnicodeBidi::Plaintext {
8656 let remaining = &cursor.items[cursor.next_item_index..];
8658 let text: String = remaining.iter()
8659 .filter_map(|i| i.as_cluster())
8660 .map(|c| c.text.as_str())
8661 .collect();
8662 match unicode_bidi::get_base_direction(text.as_str()) {
8663 unicode_bidi::Direction::Ltr => BidiDirection::Ltr,
8664 unicode_bidi::Direction::Rtl => BidiDirection::Rtl,
8665 unicode_bidi::Direction::Mixed => fragment_constraints.direction.unwrap_or(BidiDirection::Ltr),
8667 }
8668 } else {
8669 fragment_constraints.direction.unwrap_or(BidiDirection::Ltr)
8670 };
8671
8672 if let Some(msgs) = debug_messages {
8673 msgs.push(LayoutDebugMessage::info(format!(
8674 "[PFLayout] Base direction: {:?} (from CSS), Text align: {:?}",
8675 base_direction, fragment_constraints.text_align
8676 )));
8677 }
8678
8679 let balanced_lines_per_column: Option<usize> = if num_columns > 1
8691 && fragment_constraints.shape_boundaries.is_empty()
8692 && !is_min_content
8693 && !is_max_content
8694 {
8695 let mut probe = cursor.clone();
8696 let mut probe_col_constraints = fragment_constraints.clone();
8697 probe_col_constraints.available_width = AvailableSpace::Definite(column_width);
8698 let probe_line_height = fragment_constraints.resolved_line_height();
8699 let iter_cap = probe.items.len().saturating_mul(4).max(64);
8701 let mut total_lines = 0usize;
8702 let mut probe_y = 0.0_f32;
8703 let mut probe_guard = 0usize;
8704 while !probe.is_done() && probe_guard < iter_cap {
8705 probe_guard += 1;
8706 let lc = get_line_constraints(probe_y, probe_line_height, &probe_col_constraints, &mut None);
8707 if lc.segments.is_empty() {
8708 break;
8709 }
8710 let (probe_line, _) = break_one_line(
8711 &mut probe,
8712 &lc,
8713 false,
8714 hyphenator.as_ref(),
8715 fonts,
8716 fragment_constraints.line_break,
8717 fragment_constraints.white_space_mode,
8718 fragment_constraints.overflow_wrap,
8719 );
8720 if probe_line.is_empty() {
8721 break;
8722 }
8723 total_lines += 1;
8724 probe_y += probe_line_height;
8725 }
8726 (total_lines > 0).then(|| total_lines.div_ceil(num_columns as usize).max(1))
8727 } else {
8728 None
8729 };
8730
8731 'column_loop: while current_column < num_columns {
8732 if let Some(msgs) = debug_messages {
8733 msgs.push(LayoutDebugMessage::info(format!(
8734 "\n-- Starting Column {current_column} --"
8735 )));
8736 }
8737 let column_start_x =
8738 (column_width + fragment_constraints.column_gap) * current_column as f32;
8739 let mut line_top_y = 0.0;
8740 let mut line_index = 0;
8741 let mut empty_segment_count = 0; let mut is_after_forced_break = false;
8743 let column_item_start = positioned_items.len();
8748 let mut line_bands: Vec<(usize, f32, f32)> = Vec::new();
8749
8750 #[allow(clippy::no_effect_underscore_binding)] let mut _az_line_iters: usize = 0;
8758 while !cursor.is_done() {
8759 #[cfg(feature = "web_lift")]
8760 {
8761 _az_line_iters += 1;
8762 unsafe { crate::az_mark((0x60BC4) as u32, (_az_line_iters as u32 | 0xC0DE0000) as u32); }
8763 if _az_line_iters > 4096 {
8764 break;
8765 }
8766 }
8767 if let Some(max_height) = fragment_constraints.available_height {
8768 if line_top_y >= max_height {
8769 if let Some(msgs) = debug_messages {
8770 msgs.push(LayoutDebugMessage::info(format!(
8771 " Column full (pen {line_top_y} >= height {max_height}), breaking to next column."
8772 )));
8773 }
8774 break;
8775 }
8776 }
8777
8778 if let Some(clamp) = fragment_constraints.line_clamp {
8779 if line_index >= clamp.get() {
8780 break;
8781 }
8782 }
8783
8784 if let Some(budget) = balanced_lines_per_column {
8788 if current_column + 1 < num_columns && line_index >= budget {
8789 break;
8790 }
8791 }
8792
8793 let mut column_constraints = fragment_constraints.clone();
8795 if is_min_content {
8798 column_constraints.available_width = AvailableSpace::MinContent;
8799 } else if is_max_content {
8800 column_constraints.available_width = AvailableSpace::MaxContent;
8801 } else {
8802 column_constraints.available_width = AvailableSpace::Definite(column_width);
8803 }
8804 let line_constraints = get_line_constraints(
8805 line_top_y,
8806 fragment_constraints.resolved_line_height(),
8807 &column_constraints,
8808 debug_messages,
8809 );
8810
8811 if line_constraints.segments.is_empty() {
8812 empty_segment_count += 1;
8813 if let Some(msgs) = debug_messages {
8814 msgs.push(LayoutDebugMessage::info(format!(
8815 " No available segments at y={line_top_y}, skipping to next line. (empty count: \
8816 {empty_segment_count}/{MAX_EMPTY_SEGMENTS})"
8817 )));
8818 }
8819
8820 if empty_segment_count >= MAX_EMPTY_SEGMENTS {
8822 if let Some(msgs) = debug_messages {
8823 msgs.push(LayoutDebugMessage::warning(format!(
8824 " [WARN] Reached maximum empty segment count ({MAX_EMPTY_SEGMENTS}). Breaking to \
8825 prevent infinite loop."
8826 )));
8827 msgs.push(LayoutDebugMessage::warning(
8828 " This likely means the shape constraints are too restrictive or \
8829 positioned incorrectly."
8830 .to_string(),
8831 ));
8832 msgs.push(LayoutDebugMessage::warning(format!(
8833 " Current y={line_top_y}, shape boundaries might be outside this range."
8834 )));
8835 }
8836 break;
8837 }
8838
8839 if !fragment_constraints.shape_boundaries.is_empty() && empty_segment_count > 50 {
8842 let max_shape_y: f32 = fragment_constraints
8844 .shape_boundaries
8845 .iter()
8846 .map(|shape| {
8847 match shape {
8848 ShapeBoundary::Circle { center, radius } => center.y + radius,
8849 ShapeBoundary::Ellipse { center, radii } => center.y + radii.height,
8850 ShapeBoundary::Polygon { points } => {
8851 points.iter().map(|p| p.y).fold(0.0, f32::max)
8852 }
8853 ShapeBoundary::Rectangle(rect) => rect.y + rect.height,
8854 ShapeBoundary::Path { segments } => segments
8855 .iter()
8856 .filter_map(|s| match s {
8857 PathSegment::MoveTo(p) | PathSegment::LineTo(p) => Some(p.y),
8858 PathSegment::CurveTo { end, .. }
8859 | PathSegment::QuadTo { end, .. } => Some(end.y),
8860 PathSegment::Arc { center, radius, .. } => {
8861 Some(center.y + radius)
8862 }
8863 PathSegment::Close => None,
8864 })
8865 .fold(0.0, f32::max),
8866 }
8867 })
8868 .fold(0.0, f32::max);
8869
8870 if line_top_y > max_shape_y + 100.0 {
8871 if let Some(msgs) = debug_messages {
8872 msgs.push(LayoutDebugMessage::info(format!(
8873 " [INFO] Current y={line_top_y} is far beyond maximum shape extent y={max_shape_y}. \
8874 Breaking layout."
8875 )));
8876 msgs.push(LayoutDebugMessage::info(
8877 " Shape boundaries exist but no segments available - text cannot \
8878 fit in shape."
8879 .to_string(),
8880 ));
8881 }
8882 break;
8883 }
8884 }
8885
8886 line_top_y += fragment_constraints.resolved_line_height();
8887 continue;
8888 }
8889
8890 empty_segment_count = 0;
8892
8893 let effective_overflow_wrap = if is_min_content && fragment_constraints.overflow_wrap == OverflowWrap::Anywhere {
8898 OverflowWrap::Anywhere
8899 } else if is_min_content && fragment_constraints.overflow_wrap == OverflowWrap::BreakWord {
8900 OverflowWrap::Normal
8901 } else {
8902 fragment_constraints.overflow_wrap
8903 };
8904
8905 let (mut line_items, was_hyphenated) =
8912 break_one_line(cursor, &line_constraints, false, hyphenator.as_ref(), fonts, fragment_constraints.line_break, fragment_constraints.white_space_mode, effective_overflow_wrap);
8913 if line_items.is_empty() {
8914 if let Some(msgs) = debug_messages {
8915 msgs.push(LayoutDebugMessage::info(
8916 " Break returned no items. Ending column.".to_string(),
8917 ));
8918 }
8919 break;
8920 }
8921
8922 let line_text_before_rev: String = line_items
8923 .iter()
8924 .filter_map(|i| i.as_cluster())
8925 .map(|c| c.text.as_str())
8926 .collect();
8927 if let Some(msgs) = debug_messages {
8928 msgs.push(LayoutDebugMessage::info(format!(
8929 "[PFLayout] Line items from breaker (visual order): [{line_text_before_rev}]"
8931 )));
8932 }
8933
8934 apply_l2_visual_reversal(&mut line_items);
8939
8940 if let Some(msgs) = debug_messages {
8941 let after: String = line_items
8942 .iter()
8943 .filter_map(|i| i.as_cluster())
8944 .map(|c| c.text.as_str())
8945 .collect();
8946 if after != line_text_before_rev {
8947 msgs.push(LayoutDebugMessage::info(format!(
8948 "[PFLayout] Line items after L2 reversal: [{after}]"
8949 )));
8950 }
8951 }
8952
8953 let line_ends_with_forced_break = line_items.iter().any(|item| matches!(item, ShapedItem::Break { .. }));
8955
8956 let is_last_line = cursor.is_done() && !was_hyphenated;
8958 let effective_align = resolve_effective_alignment(
8959 fragment_constraints.text_align,
8960 fragment_constraints.text_align_last,
8961 is_last_line || line_ends_with_forced_break,
8962 );
8963
8964 let (mut line_pos_items, line_height) = position_one_line(
8965 &line_items,
8966 &line_constraints,
8967 line_top_y,
8968 line_index,
8969 effective_align,
8970 base_direction,
8971 is_last_line,
8972 fragment_constraints,
8973 debug_messages,
8974 fonts,
8975 is_after_forced_break,
8976 );
8977
8978 is_after_forced_break = line_ends_with_forced_break;
8980
8981 for item in &mut line_pos_items {
8982 item.position.x += column_start_x;
8983 }
8984
8985 let band_height = line_height.max(fragment_constraints.resolved_line_height());
8987 line_bands.push((line_index, line_top_y, band_height));
8988 line_top_y += band_height;
8989 line_index += 1;
8990 positioned_items.extend(line_pos_items);
8991 }
8992
8993 if fragment_constraints.writing_mode == Some(WritingMode::VerticalRl) {
9000 let block_extent = line_top_y;
9001 for item in &mut positioned_items[column_item_start..] {
9002 if let Some(&(_, t, h)) =
9003 line_bands.iter().find(|(li, _, _)| *li == item.line_index)
9004 {
9005 item.position.x += block_extent - t - t - h;
9008 }
9009 }
9010 }
9011 current_column += 1;
9012 }
9013
9014 if let Some(msgs) = debug_messages {
9015 msgs.push(LayoutDebugMessage::info(format!(
9016 "--- Exiting perform_fragment_layout, positioned {} items ---",
9017 positioned_items.len()
9018 )));
9019 }
9020
9021 let mut layout = UnifiedLayout {
9022 items: positioned_items,
9023 overflow: OverflowInfo::default(),
9024 };
9025
9026 let calculated_bounds = layout.bounds();
9028
9029 layout.overflow.unclipped_bounds = calculated_bounds;
9035
9036 if let Some(msgs) = debug_messages {
9037 msgs.push(LayoutDebugMessage::info(format!(
9038 "--- Calculated bounds: width={}, height={} ---",
9039 calculated_bounds.width, calculated_bounds.height
9040 )));
9041 }
9042
9043 Ok(layout)
9044}
9045
9046#[allow(clippy::cognitive_complexity)] #[allow(clippy::too_many_lines)] pub fn break_one_line<T: ParsedFontTrait>(
9107 cursor: &mut BreakCursor<'_>,
9108 line_constraints: &LineConstraints,
9109 is_vertical: bool,
9110 hyphenator: Option<&Standard>,
9111 fonts: &LoadedFonts<T>,
9112 line_break: LineBreakStrictness,
9113 white_space_mode: WhiteSpaceMode,
9114 overflow_wrap: OverflowWrap,
9115) -> (Vec<ShapedItem>, bool) {
9116 let mut line_items = Vec::new();
9117 let mut current_width = 0.0;
9118
9119 if cursor.is_done() {
9120 return (Vec::new(), false);
9121 }
9122
9123 let strip_leading = matches!(
9131 white_space_mode,
9132 WhiteSpaceMode::Normal | WhiteSpaceMode::Nowrap | WhiteSpaceMode::PreLine
9133 );
9134 if strip_leading {
9135 while !cursor.is_done() {
9136 let next_unit = cursor.peek_next_unit();
9137 if next_unit.is_empty() {
9138 break;
9139 }
9140 if next_unit.len() == 1 && is_collapsible_whitespace(&next_unit[0]) {
9141 cursor.consume(1);
9142 } else {
9143 break;
9144 }
9145 }
9146 }
9147
9148 let no_wrap = matches!(white_space_mode, WhiteSpaceMode::Nowrap | WhiteSpaceMode::Pre);
9152
9153 if no_wrap {
9154 loop {
9157 let next_unit = cursor.peek_next_unit();
9158 if next_unit.is_empty() {
9159 break;
9160 }
9161 if let Some(ShapedItem::Break { .. }) = next_unit.first() {
9162 line_items.push(next_unit[0].clone());
9163 cursor.consume(1);
9164 return (line_items, false);
9165 }
9166 line_items.extend_from_slice(&next_unit);
9167 cursor.consume(next_unit.len());
9168 }
9169 } else {
9170
9171 loop {
9172 let next_unit = if line_break == LineBreakStrictness::Anywhere {
9174 cursor.peek_next_single_item()
9175 } else {
9176 cursor.peek_next_unit()
9177 };
9178 if next_unit.is_empty() {
9179 break; }
9181
9182 if let Some(ShapedItem::Break { .. }) = next_unit.first() {
9183 line_items.push(next_unit[0].clone());
9184 cursor.consume(1);
9185 return (line_items, false);
9186 }
9187
9188 if line_constraints.is_min_content
9196 && !line_items.is_empty()
9197 && next_unit.len() == 1
9198 && is_break_opportunity_with_word_break(&next_unit[0], cursor.word_break, cursor.hyphens)
9199 {
9200 break;
9201 }
9202
9203 let unit_width: f32 = next_unit
9204 .iter()
9205 .map(|item| get_item_measure_with_spacing(item, is_vertical))
9206 .sum();
9207 let available_width = line_constraints.total_available - current_width;
9208
9209 if unit_width <= available_width {
9211 line_items.extend_from_slice(&next_unit);
9212 current_width += unit_width;
9213 cursor.consume(next_unit.len());
9214 } else {
9215 if line_break != LineBreakStrictness::Anywhere {
9217 if let Some(hyphenator) = hyphenator {
9218 if !is_break_opportunity(next_unit.last().unwrap()) {
9219 if let Some(hyphenation_result) = try_hyphenate_word_cluster(
9220 &next_unit,
9221 available_width,
9222 is_vertical,
9223 hyphenator,
9224 fonts,
9225 ) {
9226 line_items.extend(hyphenation_result.line_part);
9227 cursor.consume(next_unit.len());
9228 cursor.partial_remainder = hyphenation_result.remainder_part;
9229 return (line_items, true);
9230 }
9231 }
9232 }
9233 }
9234
9235 if line_items.is_empty() {
9243 match overflow_wrap {
9244 OverflowWrap::Anywhere | OverflowWrap::BreakWord => {
9245 let avail = line_constraints.total_available;
9252 for item in &next_unit {
9253 let item_w = get_item_measure_with_spacing(item, is_vertical);
9254 if !line_items.is_empty() && avail > 0.0 && current_width + item_w > avail {
9258 break;
9259 }
9260 line_items.push(item.clone());
9261 current_width += item_w;
9262 }
9269 let consumed = line_items.len().max(1);
9270 if line_items.is_empty() {
9271 line_items.push(next_unit[0].clone());
9272 }
9273 cursor.consume(consumed);
9274 }
9275 OverflowWrap::Normal => {
9276 line_items.extend_from_slice(&next_unit);
9280 cursor.consume(next_unit.len());
9281 }
9282 }
9283 }
9284 break;
9285 }
9286 }
9287
9288 } let strip_trailing = matches!(
9298 white_space_mode,
9299 WhiteSpaceMode::Normal | WhiteSpaceMode::Nowrap | WhiteSpaceMode::PreLine
9300 );
9301 if strip_trailing {
9302 while let Some(last) = line_items.last() {
9303 if is_collapsible_whitespace(last) {
9304 line_items.pop();
9305 } else {
9306 break;
9307 }
9308 }
9309 }
9310
9311 (line_items, false)
9312}
9313
9314#[derive(Debug, Clone)]
9316pub struct HyphenationBreak {
9317 pub char_len_on_line: usize,
9319 pub width_on_line: f32,
9321 pub line_part: Vec<ShapedItem>,
9323 pub hyphen_item: ShapedItem,
9325 pub remainder_part: Vec<ShapedItem>,
9328}
9329
9330#[allow(clippy::cast_precision_loss)] #[must_use] pub fn find_all_hyphenation_breaks<T: ParsedFontTrait>(
9336 word_clusters: &[ShapedCluster],
9337 hyphenator: &Standard,
9338 is_vertical: bool, fonts: &LoadedFonts<T>,
9340) -> Option<Vec<HyphenationBreak>> {
9341 if word_clusters.is_empty() {
9342 return None;
9343 }
9344
9345 let mut word_string = String::new();
9347 let mut char_map = Vec::new();
9348 let mut current_width = 0.0;
9349
9350 for (cluster_idx, cluster) in word_clusters.iter().enumerate() {
9351 for (char_byte_offset, _ch) in cluster.text.char_indices() {
9352 let glyph_idx = cluster
9353 .glyphs
9354 .iter()
9355 .rposition(|g| g.cluster_offset as usize <= char_byte_offset)
9356 .unwrap_or(0);
9357 let glyph = &cluster.glyphs[glyph_idx];
9358
9359 let num_chars_in_glyph = cluster.text[glyph.cluster_offset as usize..]
9360 .chars()
9361 .count();
9362 let advance_per_char = if is_vertical {
9363 glyph.vertical_advance
9364 } else {
9365 glyph.advance
9366 } / (num_chars_in_glyph as f32).max(1.0);
9367
9368 current_width += advance_per_char;
9369 char_map.push((cluster_idx, glyph_idx, current_width));
9370 }
9371 word_string.push_str(&cluster.text);
9372 }
9373
9374 let opportunities = hyphenator.hyphenate(&word_string);
9377 if opportunities.breaks.is_empty() {
9378 return None;
9379 }
9380
9381 let last_cluster = word_clusters.last().unwrap();
9382 let last_glyph = last_cluster.glyphs.last().unwrap();
9383 let style = last_cluster.style.clone();
9384
9385 let font = fonts.get_by_hash(last_glyph.font_hash)?;
9387 let (hyphen_glyph_id, hyphen_advance) =
9388 font.get_hyphen_glyph_and_advance(style.font_size_px)?;
9389
9390 let mut possible_breaks = Vec::new();
9391
9392 for &break_char_idx in &opportunities.breaks {
9394 if break_char_idx == 0 || break_char_idx > char_map.len() {
9397 continue;
9398 }
9399
9400 let (_, _, width_at_break) = char_map[break_char_idx - 1];
9401
9402 let line_part: Vec<ShapedItem> = word_clusters[..break_char_idx]
9404 .iter()
9405 .map(|c| ShapedItem::Cluster(c.clone()))
9406 .collect();
9407
9408 let remainder_part: Vec<ShapedItem> = word_clusters[break_char_idx..]
9410 .iter()
9411 .map(|c| ShapedItem::Cluster(c.clone()))
9412 .collect();
9413
9414 let hyphen_item = ShapedItem::Cluster(ShapedCluster {
9415 text: "-".to_string(),
9416 source_cluster_id: GraphemeClusterId {
9417 source_run: u32::MAX,
9418 start_byte_in_run: u32::MAX,
9419 },
9420 source_content_index: ContentIndex {
9421 run_index: u32::MAX,
9422 item_index: u32::MAX,
9423 },
9424 source_node_id: None, glyphs: smallvec![ShapedGlyph {
9426 kind: GlyphKind::Hyphen,
9427 glyph_id: hyphen_glyph_id,
9428 font_hash: last_glyph.font_hash,
9429 font_metrics: last_glyph.font_metrics,
9430 cluster_offset: 0,
9431 script: Script::Latin,
9432 advance: hyphen_advance,
9433 kerning: 0.0,
9434 offset: Point::default(),
9435 style: style.clone(),
9436 vertical_advance: hyphen_advance,
9437 vertical_offset: Point::default(),
9438 }],
9439 advance: hyphen_advance,
9440 direction: BidiDirection::Ltr,
9441 style: style.clone(),
9442 marker_position_outside: None,
9443 is_first_fragment: true,
9444 is_last_fragment: true,
9445 });
9446
9447 possible_breaks.push(HyphenationBreak {
9448 char_len_on_line: break_char_idx,
9449 width_on_line: width_at_break + hyphen_advance,
9450 line_part,
9451 hyphen_item,
9452 remainder_part,
9453 });
9454 }
9455
9456 Some(possible_breaks)
9457}
9458
9459fn try_hyphenate_word_cluster<T: ParsedFontTrait>(
9461 word_items: &[ShapedItem],
9462 remaining_width: f32,
9463 is_vertical: bool,
9464 hyphenator: &Standard,
9465 fonts: &LoadedFonts<T>,
9466) -> Option<HyphenationResult> {
9467 let word_clusters: Vec<ShapedCluster> = word_items
9468 .iter()
9469 .filter_map(|item| item.as_cluster().cloned())
9470 .collect();
9471
9472 if word_clusters.is_empty() {
9473 return None;
9474 }
9475
9476 let all_breaks = find_all_hyphenation_breaks(&word_clusters, hyphenator, is_vertical, fonts)?;
9477
9478 if let Some(best_break) = all_breaks
9479 .into_iter()
9480 .rfind(|b| b.width_on_line <= remaining_width)
9481 {
9482 let mut line_part = best_break.line_part;
9483 line_part.push(best_break.hyphen_item);
9484
9485 return Some(HyphenationResult {
9486 line_part,
9487 remainder_part: best_break.remainder_part,
9488 });
9489 }
9490
9491 None
9492}
9493
9494#[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>(
9572 line_items: &[ShapedItem],
9573 line_constraints: &LineConstraints,
9574 line_top_y: f32,
9575 line_index: usize,
9576 text_align: TextAlign,
9577 base_direction: BidiDirection,
9578 is_last_line: bool,
9579 constraints: &UnifiedConstraints,
9580 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
9581 fonts: &LoadedFonts<T>,
9582 is_after_forced_break: bool,
9583) -> (Vec<PositionedItem>, f32) {
9584 let line_text: String = line_items
9585 .iter()
9586 .filter_map(|i| i.as_cluster())
9587 .map(|c| c.text.as_str())
9588 .collect();
9589 if let Some(msgs) = debug_messages {
9590 msgs.push(LayoutDebugMessage::info(format!(
9591 "\n--- Entering position_one_line for line: [{line_text}] ---"
9592 )));
9593 }
9594 let physical_align = match (text_align, base_direction) {
9598 (TextAlign::Start, BidiDirection::Ltr) => TextAlign::Left,
9599 (TextAlign::Start, BidiDirection::Rtl) => TextAlign::Right,
9600 (TextAlign::End, BidiDirection::Ltr) => TextAlign::Right,
9601 (TextAlign::End, BidiDirection::Rtl) => TextAlign::Left,
9602 (other, _) => other,
9604 };
9605 if let Some(msgs) = debug_messages {
9606 msgs.push(LayoutDebugMessage::info(format!(
9607 "[Pos1Line] Physical align: {physical_align:?}"
9608 )));
9609 }
9610
9611 if line_items.is_empty() {
9615 return (Vec::new(), 0.0);
9616 }
9617 let mut positioned = Vec::new();
9618 let is_vertical = constraints.is_vertical();
9619
9620 let (content_ascent, content_descent) = calculate_line_metrics(line_items, constraints.vertical_align, constraints);
9625
9626 let strut_ad = constraints.strut_ascent + constraints.strut_descent;
9633 let strut_leading_half = (constraints.resolved_line_height() - strut_ad) / 2.0;
9634 let strut_above = constraints.strut_ascent + strut_leading_half;
9635 let strut_below = constraints.strut_descent + strut_leading_half;
9636 let line_ascent = content_ascent.max(strut_above);
9637 let line_descent = content_descent.max(strut_below);
9638 let line_box_height = line_ascent + line_descent;
9639
9640 let line_baseline_y = line_top_y + line_ascent;
9642
9643 let mut item_cursor = 0;
9645 let is_first_line_of_para = line_index == 0; for (segment_idx, segment) in line_constraints.segments.iter().enumerate() {
9648 if item_cursor >= line_items.len() {
9649 break;
9650 }
9651
9652 let mut segment_items = Vec::new();
9654 let mut current_segment_width = 0.0;
9655 while item_cursor < line_items.len() {
9656 let item = &line_items[item_cursor];
9657 let item_measure = get_item_measure(item, is_vertical);
9658 if current_segment_width + item_measure > segment.width && !segment_items.is_empty() {
9660 break;
9661 }
9662 segment_items.push(item.clone());
9663 current_segment_width += item_measure;
9664 item_cursor += 1;
9665 }
9666
9667 if segment_items.is_empty() {
9668 continue;
9669 }
9670
9671 let (extra_word_spacing, extra_char_spacing) = if (constraints.text_align == TextAlign::Justify
9678 || constraints.text_align == TextAlign::JustifyAll)
9679 && constraints.text_justify != JustifyContent::None
9680 && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
9681 && constraints.text_justify != JustifyContent::Kashida
9682 {
9683 let segment_line_constraints = LineConstraints {
9684 segments: vec![*segment],
9685 total_available: segment.width,
9686 is_min_content: false,
9687 };
9688 calculate_justification_spacing(
9689 &segment_items,
9690 &segment_line_constraints,
9691 constraints.text_justify,
9692 is_vertical,
9693 )
9694 } else {
9695 (0.0, 0.0)
9696 };
9697
9698 let justified_segment_items = if constraints.text_justify == JustifyContent::Kashida
9700 && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
9701 {
9702 let segment_line_constraints = LineConstraints {
9703 segments: vec![*segment],
9704 total_available: segment.width,
9705 is_min_content: false,
9706 };
9707 justify_kashida_and_rebuild(
9708 segment_items,
9709 &segment_line_constraints,
9710 is_vertical,
9711 debug_messages,
9712 fonts,
9713 )
9714 } else {
9715 segment_items
9716 };
9717
9718 let final_segment_width: f32 = justified_segment_items
9720 .iter()
9721 .map(|item| get_item_measure(item, is_vertical))
9722 .sum();
9723
9724 let trailing_ws_width = match constraints.white_space_mode {
9736 WhiteSpaceMode::BreakSpaces | WhiteSpaceMode::Pre => 0.0,
9737 WhiteSpaceMode::Normal | WhiteSpaceMode::Nowrap | WhiteSpaceMode::PreLine => {
9738 measure_trailing_whitespace(&justified_segment_items, is_vertical)
9739 }
9740 WhiteSpaceMode::PreWrap => {
9742 let has_forced_break = justified_segment_items.last()
9743 .is_some_and(|item| matches!(item, ShapedItem::Break { .. }));
9744 let ws_width = measure_trailing_whitespace(&justified_segment_items, is_vertical);
9745 if has_forced_break {
9746 let content_width = final_segment_width - ws_width;
9749 if content_width + ws_width > segment.width {
9750 ws_width
9751 } else {
9752 0.0
9753 }
9754 } else {
9755 ws_width }
9757 }
9758 };
9759 let effective_segment_width = final_segment_width - trailing_ws_width;
9760
9761 let remaining_space = segment.width - effective_segment_width;
9764
9765 let is_indefinite_width = segment.width.is_infinite() || segment.width > 1e30;
9771 let alignment_offset = if is_indefinite_width {
9773 0.0 } else {
9775 match physical_align {
9776 TextAlign::Center => remaining_space / 2.0,
9777 TextAlign::Right => remaining_space,
9778 TextAlign::Justify | TextAlign::JustifyAll
9779 if remaining_space > 0.0
9780 && extra_word_spacing == 0.0
9781 && extra_char_spacing == 0.0 =>
9782 {
9783 remaining_space / 2.0
9786 }
9787 _ => 0.0, }
9789 };
9790
9791 let mut main_axis_pen = segment.start_x + alignment_offset;
9792 if let Some(msgs) = debug_messages {
9793 msgs.push(LayoutDebugMessage::info(format!(
9794 "[Pos1Line] Segment width: {}, Item width: {}, Remaining space: {}, Initial pen: \
9795 {}",
9796 segment.width, final_segment_width, remaining_space, main_axis_pen
9797 )));
9798 }
9799
9800 if segment_idx == 0 {
9803 let is_indent_target = if constraints.text_indent_each_line {
9804 is_first_line_of_para || is_after_forced_break
9806 } else {
9807 is_first_line_of_para
9809 };
9810 let should_indent = if constraints.text_indent_hanging {
9812 !is_indent_target
9813 } else {
9814 is_indent_target
9815 };
9816 if should_indent {
9817 main_axis_pen += constraints.text_indent;
9818 }
9819 }
9820
9821 let total_marker_width: f32 = justified_segment_items
9824 .iter()
9825 .filter_map(|item| {
9826 if let ShapedItem::Cluster(c) = item {
9827 if c.marker_position_outside == Some(true) {
9828 return Some(get_item_measure(item, is_vertical));
9829 }
9830 }
9831 None
9832 })
9833 .sum();
9834
9835 let marker_spacing = 4.0; let mut marker_pen = if total_marker_width > 0.0 {
9838 -(total_marker_width + marker_spacing)
9839 } else {
9840 0.0
9841 };
9842
9843 let inline_offsets: Vec<(f32, f32)> = {
9869 let items_slice: &[ShapedItem] = &justified_segment_items;
9870 items_slice.iter().enumerate().map(|(idx, item)| {
9871 if let ShapedItem::Cluster(c) = item {
9872 if let Some(border) = c.style.border.as_ref() {
9873 if border.has_chrome() {
9874 let style_ptr = Arc::as_ptr(&c.style);
9875 let prev_same_span = idx > 0 && items_slice[idx - 1]
9876 .as_cluster()
9877 .is_some_and(|pc| Arc::as_ptr(&pc.style) == style_ptr);
9878 let next_same_span = idx + 1 < items_slice.len() && items_slice[idx + 1]
9879 .as_cluster()
9880 .is_some_and(|nc| Arc::as_ptr(&nc.style) == style_ptr);
9881 let left = if prev_same_span { 0.0 } else { border.left_inset() };
9882 let right = if next_same_span { 0.0 } else { border.right_inset() };
9883 return (left, right);
9884 }
9885 }
9886 }
9887 (0.0, 0.0)
9888 }).collect()
9889 };
9890 for (inline_offset_idx, item) in justified_segment_items.into_iter().enumerate() {
9891 let (item_ascent, item_descent) = get_item_vertical_metrics(&item, constraints);
9892 let effective_align = get_item_vertical_align(&item)
9894 .unwrap_or(constraints.vertical_align);
9895 let item_baseline_pos = match effective_align {
9899 VerticalAlign::Top => line_top_y + item_ascent,
9903 VerticalAlign::Middle => {
9905 let half_x_height = constraints.strut_x_height / 2.0;
9906 line_baseline_y + half_x_height - f32::midpoint(item_ascent, item_descent) + item_ascent
9907 }
9908 VerticalAlign::Bottom => line_top_y + line_box_height - item_descent,
9910 VerticalAlign::Sub => line_baseline_y + line_ascent * SUBSCRIPT_OFFSET_RATIO,
9912 VerticalAlign::Super => line_baseline_y - line_ascent * SUPERSCRIPT_OFFSET_RATIO,
9915 VerticalAlign::TextTop => (line_baseline_y - constraints.strut_ascent) + item_ascent,
9918 VerticalAlign::TextBottom => (line_baseline_y + constraints.strut_descent) - item_descent,
9921 VerticalAlign::Offset(offset) => line_baseline_y - offset,
9923 VerticalAlign::Baseline => line_baseline_y,
9927 };
9928
9929 let item_measure = get_item_measure(&item, is_vertical);
9931
9932 let (left_inset, right_inset) = if inline_offset_idx < inline_offsets.len() {
9934 inline_offsets[inline_offset_idx]
9935 } else {
9936 (0.0, 0.0)
9937 };
9938 main_axis_pen += left_inset;
9939
9940 let position = if is_vertical {
9941 Point {
9942 x: item_baseline_pos - item_ascent,
9943 y: main_axis_pen,
9944 }
9945 } else {
9946 if let Some(msgs) = debug_messages {
9947 msgs.push(LayoutDebugMessage::info(format!(
9948 "[Pos1Line] is_vertical=false, main_axis_pen={main_axis_pen}, item_baseline_pos={item_baseline_pos}, \
9949 item_ascent={item_ascent}"
9950 )));
9951 }
9952
9953 let x_position = if let ShapedItem::Cluster(cluster) = &item {
9955 if cluster.marker_position_outside == Some(true) {
9956 let marker_width = item_measure;
9958 if let Some(msgs) = debug_messages {
9959 msgs.push(LayoutDebugMessage::info(format!(
9960 "[Pos1Line] Outside marker detected! width={marker_width}, positioning at \
9961 marker_pen={marker_pen}"
9962 )));
9963 }
9964 let pos = marker_pen;
9965 marker_pen += marker_width; pos
9967 } else {
9968 main_axis_pen
9969 }
9970 } else {
9971 main_axis_pen
9972 };
9973
9974 Point {
9975 y: item_baseline_pos - item_ascent,
9976 x: x_position,
9977 }
9978 };
9979
9980 let item_text = item
9982 .as_cluster()
9983 .map_or("[OBJ]", |c| c.text.as_str());
9984 if let Some(msgs) = debug_messages {
9985 msgs.push(LayoutDebugMessage::info(format!(
9986 "[Pos1Line] Positioning item '{item_text}' at pen_x={main_axis_pen}"
9987 )));
9988 }
9989 positioned.push(PositionedItem {
9990 item: item.clone(),
9991 position,
9992 line_index,
9993 });
9994
9995 let is_outside_marker = if let ShapedItem::Cluster(c) = &item {
9997 c.marker_position_outside == Some(true)
9998 } else {
9999 false
10000 };
10001
10002 if !is_outside_marker {
10003 main_axis_pen += item_measure;
10004 main_axis_pen += right_inset;
10006 }
10007
10008 let is_cursive = if let ShapedItem::Cluster(c) = &item { is_cursive_script_cluster(c) } else { false };
10011 if !is_outside_marker && extra_char_spacing > 0.0 && can_justify_after(&item) && !is_cursive {
10012 main_axis_pen += extra_char_spacing;
10013 }
10014 if let ShapedItem::Cluster(c) = &item {
10018 if !is_outside_marker {
10019 if !is_cursive_script_cluster(c) {
10028 let letter_spacing_px = c.style.letter_spacing.resolve_px(c.style.font_size_px);
10029 main_axis_pen += letter_spacing_px;
10030 }
10031 if is_word_separator(&item) {
10033 let word_spacing_px = c.style.word_spacing.resolve_px(c.style.font_size_px);
10034 main_axis_pen += word_spacing_px;
10035 main_axis_pen += extra_word_spacing;
10036 }
10037 }
10038 }
10039 }
10040 }
10041
10042 (positioned, line_box_height)
10043}
10044
10045fn calculate_alignment_offset(
10047 items: &[ShapedItem],
10048 line_constraints: &LineConstraints,
10049 align: TextAlign,
10050 is_vertical: bool,
10051 constraints: &UnifiedConstraints,
10052) -> f32 {
10053 if let Some(segment) = line_constraints.segments.first() {
10055 let total_width: f32 = items
10058 .iter()
10059 .map(|item| get_item_measure_with_spacing(item, is_vertical))
10060 .sum();
10061
10062 let available_width = if constraints.segment_alignment == SegmentAlignment::Total {
10063 line_constraints.total_available
10064 } else {
10065 segment.width
10066 };
10067
10068 if total_width >= available_width {
10069 return 0.0; }
10071
10072 let remaining_space = available_width - total_width;
10073
10074 match align {
10075 TextAlign::Center => remaining_space / 2.0,
10076 TextAlign::Right => remaining_space,
10077 _ => 0.0, }
10079 } else {
10080 0.0
10081 }
10082}
10083
10084#[allow(clippy::cast_precision_loss)] fn calculate_justification_spacing(
10103 items: &[ShapedItem],
10104 line_constraints: &LineConstraints,
10105 text_justify: JustifyContent,
10106 is_vertical: bool,
10107) -> (f32, f32) {
10108 let total_width: f32 = items
10110 .iter()
10111 .map(|item| get_item_measure(item, is_vertical))
10112 .sum();
10113 let available_width = line_constraints.total_available;
10114
10115 if total_width >= available_width || available_width <= 0.0 {
10116 return (0.0, 0.0);
10117 }
10118
10119 let extra_space = available_width - total_width;
10120
10121 match text_justify {
10123 JustifyContent::InterWord => {
10124 let space_count = items.iter().filter(|item| is_word_separator(item)).count();
10126 if space_count > 0 {
10127 (extra_space / space_count as f32, 0.0)
10128 } else {
10129 (0.0, 0.0) }
10131 }
10132 JustifyContent::InterCharacter | JustifyContent::Distribute => {
10133 let gap_count = items
10135 .iter()
10136 .enumerate()
10137 .filter(|(i, item)| *i < items.len() - 1 && can_justify_after(item))
10138 .count();
10139 if gap_count > 0 {
10140 (0.0, extra_space / gap_count as f32)
10141 } else {
10142 (0.0, 0.0) }
10144 }
10145 _ => (0.0, 0.0),
10147 }
10148}
10149
10150#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines)] pub fn justify_kashida_and_rebuild<T: ParsedFontTrait>(
10158 items: Vec<ShapedItem>,
10159 line_constraints: &LineConstraints,
10160 is_vertical: bool,
10161 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
10162 fonts: &LoadedFonts<T>,
10163) -> Vec<ShapedItem> {
10164 if let Some(msgs) = debug_messages {
10165 msgs.push(LayoutDebugMessage::info(
10166 "\n--- Entering justify_kashida_and_rebuild ---".to_string(),
10167 ));
10168 }
10169 let total_width: f32 = items
10170 .iter()
10171 .map(|item| get_item_measure(item, is_vertical))
10172 .sum();
10173 let available_width = line_constraints.total_available;
10174 if let Some(msgs) = debug_messages {
10175 msgs.push(LayoutDebugMessage::info(format!(
10176 "Total item width: {total_width}, Available width: {available_width}"
10177 )));
10178 }
10179
10180 if total_width >= available_width || available_width <= 0.0 {
10181 if let Some(msgs) = debug_messages {
10182 msgs.push(LayoutDebugMessage::info(
10183 "No justification needed (line is full or invalid).".to_string(),
10184 ));
10185 }
10186 return items;
10187 }
10188
10189 let extra_space = available_width - total_width;
10190 if let Some(msgs) = debug_messages {
10191 msgs.push(LayoutDebugMessage::info(format!(
10192 "Extra space to fill: {extra_space}"
10193 )));
10194 }
10195
10196 let font_info = items.iter().find_map(|item| {
10197 if let ShapedItem::Cluster(c) = item {
10198 if let Some(glyph) = c.glyphs.first() {
10199 if glyph.script == Script::Arabic {
10200 if let Some(font) = fonts.get_by_hash(glyph.font_hash) {
10202 return Some((
10203 font.clone(),
10204 glyph.font_hash,
10205 glyph.font_metrics,
10206 glyph.style.clone(),
10207 ));
10208 }
10209 }
10210 }
10211 }
10212 None
10213 });
10214
10215 let (font, font_hash, font_metrics, style) = if let Some(info) = font_info {
10216 if let Some(msgs) = debug_messages {
10217 msgs.push(LayoutDebugMessage::info(
10218 "Found Arabic font for kashida.".to_string(),
10219 ));
10220 }
10221 info
10222 } else {
10223 if let Some(msgs) = debug_messages {
10224 msgs.push(LayoutDebugMessage::info(
10225 "No Arabic font found on line. Cannot insert kashidas.".to_string(),
10226 ));
10227 }
10228 return items;
10229 };
10230
10231 let (kashida_glyph_id, kashida_advance) =
10232 match font.get_kashida_glyph_and_advance(style.font_size_px) {
10233 Some((id, adv)) if adv > 0.0 => {
10234 if let Some(msgs) = debug_messages {
10235 msgs.push(LayoutDebugMessage::info(format!(
10236 "Font provides kashida glyph with advance {adv}"
10237 )));
10238 }
10239 (id, adv)
10240 }
10241 _ => {
10242 if let Some(msgs) = debug_messages {
10243 msgs.push(LayoutDebugMessage::info(
10244 "Font does not support kashida justification.".to_string(),
10245 ));
10246 }
10247 return items;
10248 }
10249 };
10250
10251 let opportunity_indices: Vec<usize> = items
10252 .windows(2)
10253 .enumerate()
10254 .filter_map(|(i, window)| {
10255 if let (ShapedItem::Cluster(cur), ShapedItem::Cluster(next)) = (&window[0], &window[1])
10256 {
10257 if is_arabic_cluster(cur)
10258 && is_arabic_cluster(next)
10259 && !is_word_separator(&window[1])
10260 {
10261 return Some(i + 1);
10262 }
10263 }
10264 None
10265 })
10266 .collect();
10267
10268 if let Some(msgs) = debug_messages {
10269 msgs.push(LayoutDebugMessage::info(format!(
10270 "Found {} kashida insertion opportunities at indices: {:?}",
10271 opportunity_indices.len(),
10272 opportunity_indices
10273 )));
10274 }
10275
10276 if opportunity_indices.is_empty() {
10277 if let Some(msgs) = debug_messages {
10278 msgs.push(LayoutDebugMessage::info(
10279 "No opportunities found. Exiting.".to_string(),
10280 ));
10281 }
10282 return items;
10283 }
10284
10285 let num_kashidas_to_insert = (extra_space / kashida_advance).floor() as usize;
10286 if let Some(msgs) = debug_messages {
10287 msgs.push(LayoutDebugMessage::info(format!(
10288 "Calculated number of kashidas to insert: {num_kashidas_to_insert}"
10289 )));
10290 }
10291
10292 if num_kashidas_to_insert == 0 {
10293 return items;
10294 }
10295
10296 let kashidas_per_point = num_kashidas_to_insert / opportunity_indices.len();
10297 let mut remainder = num_kashidas_to_insert % opportunity_indices.len();
10298 if let Some(msgs) = debug_messages {
10299 msgs.push(LayoutDebugMessage::info(format!(
10300 "Distributing kashidas: {kashidas_per_point} per point, with {remainder} remainder."
10301 )));
10302 }
10303
10304 let kashida_item = {
10305 let kashida_glyph = ShapedGlyph {
10307 kind: GlyphKind::Kashida {
10308 width: kashida_advance,
10309 },
10310 glyph_id: kashida_glyph_id,
10311 font_hash,
10312 font_metrics,
10313 style: style.clone(),
10314 script: Script::Arabic,
10315 advance: kashida_advance,
10316 kerning: 0.0,
10317 cluster_offset: 0,
10318 offset: Point::default(),
10319 vertical_advance: 0.0,
10320 vertical_offset: Point::default(),
10321 };
10322 ShapedItem::Cluster(ShapedCluster {
10323 text: "\u{0640}".to_string(),
10324 source_cluster_id: GraphemeClusterId {
10325 source_run: u32::MAX,
10326 start_byte_in_run: u32::MAX,
10327 },
10328 source_content_index: ContentIndex {
10329 run_index: u32::MAX,
10330 item_index: u32::MAX,
10331 },
10332 source_node_id: None, glyphs: smallvec![kashida_glyph],
10334 advance: kashida_advance,
10335 direction: BidiDirection::Ltr,
10336 style,
10337 marker_position_outside: None,
10338 is_first_fragment: true,
10339 is_last_fragment: true,
10340 })
10341 };
10342
10343 let mut new_items = Vec::with_capacity(items.len() + num_kashidas_to_insert);
10344 let mut last_copy_idx = 0;
10345 for &point in &opportunity_indices {
10346 new_items.extend_from_slice(&items[last_copy_idx..point]);
10347 let mut num_to_insert = kashidas_per_point;
10348 if remainder > 0 {
10349 num_to_insert += 1;
10350 remainder -= 1;
10351 }
10352 for _ in 0..num_to_insert {
10353 new_items.push(kashida_item.clone());
10354 }
10355 last_copy_idx = point;
10356 }
10357 new_items.extend_from_slice(&items[last_copy_idx..]);
10358
10359 if let Some(msgs) = debug_messages {
10360 msgs.push(LayoutDebugMessage::info(format!(
10361 "--- Exiting justify_kashida_and_rebuild, new item count: {} ---",
10362 new_items.len()
10363 )));
10364 }
10365 new_items
10366}
10367
10368fn is_arabic_cluster(cluster: &ShapedCluster) -> bool {
10370 cluster.glyphs.iter().any(|g| g.script == Script::Arabic)
10373}
10374
10375fn measure_trailing_whitespace(items: &[ShapedItem], is_vertical: bool) -> f32 {
10377 let mut trailing_ws = 0.0;
10378 for item in items.iter().rev() {
10379 if is_collapsible_whitespace(item) {
10380 trailing_ws += get_item_measure(item, is_vertical);
10381 } else {
10382 break;
10383 }
10384 }
10385 trailing_ws
10386}
10387
10388#[must_use] pub fn is_collapsible_whitespace(item: &ShapedItem) -> bool {
10393 if let ShapedItem::Cluster(c) = item {
10394 c.text.chars().all(|ch| matches!(ch,
10395 ' ' | '\t' | '\u{1680}' ))
10397 } else {
10398 false
10399 }
10400}
10401
10402pub fn is_cursive_script_cluster(c: &ShapedCluster) -> bool {
10409 c.text.chars().next().is_some_and(is_cursive_script_char)
10410}
10411
10412fn is_cursive_script_char(ch: char) -> bool {
10413 let cp = ch as u32;
10414 if (0x0600..=0x06FF).contains(&cp) { return true; }
10416 if (0x0750..=0x077F).contains(&cp) { return true; }
10417 if (0x08A0..=0x08FF).contains(&cp) { return true; }
10418 if (0xFB50..=0xFDFF).contains(&cp) { return true; }
10419 if (0xFE70..=0xFEFF).contains(&cp) { return true; }
10420 if (0x0700..=0x074F).contains(&cp) { return true; }
10422 if (0x1800..=0x18AF).contains(&cp) { return true; }
10424 if (0x07C0..=0x07FF).contains(&cp) { return true; }
10426 if (0x0840..=0x085F).contains(&cp) { return true; }
10428 if (0xA840..=0xA87F).contains(&cp) { return true; }
10430 if (0x10D00..=0x10D3F).contains(&cp) { return true; }
10432 false
10433}
10434
10435pub(crate) fn is_word_char(ch: char) -> bool {
10444 ch.is_alphanumeric() || ch == '_'
10445}
10446
10447fn cluster_is_word_boundary(cluster: &ShapedCluster) -> bool {
10451 !cluster.text.chars().any(is_word_char)
10452}
10453
10454pub fn is_word_separator(item: &ShapedItem) -> bool {
10456 if let ShapedItem::Cluster(c) = item {
10457 c.text.chars().any(is_word_separator_char)
10458 } else {
10459 false
10460 }
10461}
10462
10463#[must_use] pub fn is_no_break_space(item: &ShapedItem) -> bool {
10468 if let ShapedItem::Cluster(c) = item {
10469 c.text
10470 .chars()
10471 .any(|ch| matches!(ch, '\u{00A0}' | '\u{202F}' | '\u{2060}' | '\u{FEFF}'))
10472 } else {
10473 false
10474 }
10475}
10476
10477#[allow(clippy::match_same_arms)] const fn is_word_separator_char(c: char) -> bool {
10484 match c {
10485 '\u{0020}' => true,
10487 '\u{00A0}' => true,
10489 '\u{1680}' => true,
10491 '\u{1361}' => true,
10493 '\u{2000}'..='\u{200A}' => false,
10495 '\u{202F}' => true,
10497 '\u{205F}' => true,
10499 '\u{3000}' => false,
10501 '\u{10100}' => true,
10503 '\u{10101}' => true,
10505 '\u{1039F}' => true,
10507 '\u{1091F}' => true,
10509 _ => false,
10511 }
10512}
10513
10514#[must_use] pub fn is_zero_width_space(item: &ShapedItem) -> bool {
10520 if let ShapedItem::Cluster(c) = item {
10521 c.text.contains('\u{200B}')
10522 } else {
10523 false
10524 }
10525}
10526
10527fn can_justify_after(item: &ShapedItem) -> bool {
10529 if let ShapedItem::Cluster(c) = item {
10530 c.text.chars().last().is_some_and(|g| {
10531 !g.is_whitespace() && classify_character(g as u32) != CharacterClass::Combining
10532 })
10533 } else {
10534 false
10538 }
10539}
10540
10541#[allow(clippy::match_same_arms)] const fn classify_character(codepoint: u32) -> CharacterClass {
10546 match codepoint {
10547 0x0020 | 0x00A0 | 0x3000 => CharacterClass::Space,
10548 0x0021..=0x002F | 0x003A..=0x0040 | 0x005B..=0x0060 | 0x007B..=0x007E => {
10549 CharacterClass::Punctuation
10550 }
10551 0x4E00..=0x9FFF | 0x3400..=0x4DBF => CharacterClass::Ideograph,
10552 0x0300..=0x036F | 0x1AB0..=0x1AFF => CharacterClass::Combining,
10553 0x1800..=0x18AF => CharacterClass::Letter,
10555 _ => CharacterClass::Letter,
10556 }
10557}
10558
10559#[must_use] pub fn get_item_measure(item: &ShapedItem, is_vertical: bool) -> f32 {
10561 match item {
10562 ShapedItem::Cluster(c) => {
10563 let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
10567 c.advance + total_kerning
10568 }
10569 ShapedItem::Object { bounds, .. }
10570 | ShapedItem::CombinedBlock { bounds, .. }
10571 | ShapedItem::Tab { bounds, .. } => {
10572 if is_vertical {
10573 bounds.height
10574 } else {
10575 bounds.width
10576 }
10577 }
10578 ShapedItem::Break { .. } => 0.0,
10579 }
10580}
10581
10582#[must_use]
10591pub fn get_item_measure_with_spacing(item: &ShapedItem, is_vertical: bool) -> f32 {
10592 let base = get_item_measure(item, is_vertical);
10593 if let ShapedItem::Cluster(c) = item {
10594 let mut extra = 0.0;
10595 if !is_cursive_script_cluster(c) {
10596 extra += c.style.letter_spacing.resolve_px(c.style.font_size_px);
10597 }
10598 if is_word_separator(item) {
10599 extra += c.style.word_spacing.resolve_px(c.style.font_size_px);
10600 }
10601 base + extra
10602 } else {
10603 base
10604 }
10605}
10606
10607#[allow(clippy::match_same_arms)] fn get_line_constraints(
10611 line_y: f32,
10612 line_height: f32,
10613 constraints: &UnifiedConstraints,
10614 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
10615) -> LineConstraints {
10616 if let Some(msgs) = debug_messages {
10617 msgs.push(LayoutDebugMessage::info(format!(
10618 "\n--- Entering get_line_constraints for y={line_y} ---"
10619 )));
10620 }
10621
10622 let mut available_segments = Vec::new();
10623 if constraints.shape_boundaries.is_empty() {
10624 let segment_width = match constraints.available_width {
10635 AvailableSpace::Definite(w) => w, AvailableSpace::MaxContent => f32::MAX / 2.0, AvailableSpace::MinContent => f32::MAX / 2.0, };
10639 available_segments.push(LineSegment {
10642 start_x: 0.0,
10643 width: segment_width,
10644 priority: 0,
10645 });
10646 } else {
10647 }
10649
10650 if let Some(msgs) = debug_messages {
10651 msgs.push(LayoutDebugMessage::info(format!(
10652 "Initial available segments: {available_segments:?}"
10653 )));
10654 }
10655
10656 for (idx, exclusion) in constraints.shape_exclusions.iter().enumerate() {
10657 if let Some(msgs) = debug_messages {
10658 msgs.push(LayoutDebugMessage::info(format!(
10659 "Applying exclusion #{idx}: {exclusion:?}"
10660 )));
10661 }
10662 let exclusion_spans =
10663 get_shape_horizontal_spans(exclusion, line_y, line_height);
10664 if let Some(msgs) = debug_messages {
10665 msgs.push(LayoutDebugMessage::info(format!(
10666 " Exclusion spans at y={line_y}: {exclusion_spans:?}"
10667 )));
10668 }
10669
10670 if exclusion_spans.is_empty() {
10671 continue;
10672 }
10673
10674 let mut next_segments = Vec::new();
10675 for (excl_start, excl_end) in exclusion_spans {
10676 for segment in &available_segments {
10677 let seg_start = segment.start_x;
10678 let seg_end = segment.start_x + segment.width;
10679
10680 if seg_end > excl_start && seg_start < excl_end {
10682 if seg_start < excl_start {
10683 next_segments.push(LineSegment {
10685 start_x: seg_start,
10686 width: excl_start - seg_start,
10687 priority: segment.priority,
10688 });
10689 }
10690 if seg_end > excl_end {
10691 next_segments.push(LineSegment {
10693 start_x: excl_end,
10694 width: seg_end - excl_end,
10695 priority: segment.priority,
10696 });
10697 }
10698 } else {
10699 next_segments.push(*segment); }
10701 }
10702 available_segments = merge_segments(next_segments);
10703 next_segments = Vec::new();
10704 }
10705 if let Some(msgs) = debug_messages {
10706 msgs.push(LayoutDebugMessage::info(format!(
10707 " Segments after exclusion #{idx}: {available_segments:?}"
10708 )));
10709 }
10710 }
10711
10712 let total_width = available_segments.iter().map(|s| s.width).sum();
10713 if let Some(msgs) = debug_messages {
10714 msgs.push(LayoutDebugMessage::info(format!(
10715 "Final segments: {available_segments:?}, total available width: {total_width}"
10716 )));
10717 msgs.push(LayoutDebugMessage::info(
10718 "--- Exiting get_line_constraints ---".to_string(),
10719 ));
10720 }
10721
10722 LineConstraints {
10723 segments: available_segments,
10724 total_available: total_width,
10725 is_min_content: matches!(constraints.available_width, AvailableSpace::MinContent),
10726 }
10727}
10728
10729#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss)]
10736fn flatten_svg_to_path_segments(
10737 multipolygon: &azul_core::svg::SvgMultiPolygon,
10738 reference_box: Rect,
10739) -> Vec<PathSegment> {
10740 use azul_core::svg::SvgPathElement;
10741
10742 let mut out: Vec<PathSegment> = Vec::new();
10743
10744 for ring in multipolygon.rings.as_ref() {
10745 let elements = ring.items.as_ref();
10746 if elements.is_empty() {
10747 continue;
10748 }
10749 let start = elements[0].get_start();
10750 out.push(PathSegment::MoveTo(Point {
10751 x: reference_box.x + start.x,
10752 y: reference_box.y + start.y,
10753 }));
10754 for el in elements {
10755 match el {
10756 SvgPathElement::Line(l) => {
10757 out.push(PathSegment::LineTo(Point {
10758 x: reference_box.x + l.end.x,
10759 y: reference_box.y + l.end.y,
10760 }));
10761 }
10762 curve => {
10763 let len = curve.get_length();
10765 let steps = ((len / 4.0).ceil() as usize).clamp(1, 64);
10766 for i in 1..=steps {
10767 let offset = len * (i as f64) / (steps as f64);
10768 let t = curve.get_t_at_offset(offset);
10769 out.push(PathSegment::LineTo(Point {
10770 x: reference_box.x + curve.get_x_at_t(t) as f32,
10771 y: reference_box.y + curve.get_y_at_t(t) as f32,
10772 }));
10773 }
10774 }
10775 }
10776 }
10777 out.push(PathSegment::Close);
10778 }
10779
10780 out
10781}
10782
10783fn path_segments_line_intersection(
10788 segments: &[PathSegment],
10789 y: f32,
10790 line_height: f32,
10791) -> Vec<(f32, f32)> {
10792 let line_center_y = y + line_height / 2.0;
10793 let mut crossings: Vec<f32> = Vec::new();
10794
10795 let mut subpath: Vec<Point> = Vec::new();
10798 let flush = |subpath: &mut Vec<Point>, crossings: &mut Vec<f32>| {
10799 if subpath.len() >= 2 {
10800 for i in 0..subpath.len() {
10801 let p1 = subpath[i];
10802 let p2 = subpath[(i + 1) % subpath.len()];
10803 if (p2.y - p1.y).abs() < f32::EPSILON {
10804 continue;
10805 }
10806 let crosses = (p1.y <= line_center_y && p2.y > line_center_y)
10807 || (p1.y > line_center_y && p2.y <= line_center_y);
10808 if crosses {
10809 let t = (line_center_y - p1.y) / (p2.y - p1.y);
10810 crossings.push(t.mul_add(p2.x - p1.x, p1.x));
10811 }
10812 }
10813 }
10814 subpath.clear();
10815 };
10816
10817 for seg in segments {
10818 match seg {
10819 PathSegment::MoveTo(p) => {
10820 flush(&mut subpath, &mut crossings);
10821 subpath.push(*p);
10822 }
10823 PathSegment::LineTo(p) => subpath.push(*p),
10824 PathSegment::Close => flush(&mut subpath, &mut crossings),
10825 PathSegment::CurveTo { end, .. } | PathSegment::QuadTo { end, .. } => {
10828 subpath.push(*end);
10829 }
10830 PathSegment::Arc { center, .. } => subpath.push(*center),
10831 }
10832 }
10833 flush(&mut subpath, &mut crossings);
10834
10835 crossings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
10836 let mut spans = Vec::new();
10837 for chunk in crossings.chunks_exact(2) {
10838 if chunk[1] > chunk[0] {
10839 spans.push((chunk[0], chunk[1]));
10840 }
10841 }
10842 spans
10843}
10844
10845#[allow(clippy::suboptimal_flops)] fn get_shape_horizontal_spans(
10849 shape: &ShapeBoundary,
10850 y: f32,
10851 line_height: f32,
10852) -> Vec<(f32, f32)> {
10853 match shape {
10854 ShapeBoundary::Rectangle(rect) => {
10855 let line_start = y;
10858 let line_end = y + line_height;
10859 let rect_start = rect.y;
10860 let rect_end = rect.y + rect.height;
10861
10862 if line_start < rect_end && line_end > rect_start {
10863 vec![(rect.x, rect.x + rect.width)]
10864 } else {
10865 vec![]
10866 }
10867 }
10868 ShapeBoundary::Circle { center, radius } => {
10869 let line_center_y = y + line_height / 2.0;
10870 let dy = (line_center_y - center.y).abs();
10871 if dy <= *radius {
10872 let dx = (radius.powi(2) - dy.powi(2)).sqrt();
10873 vec![(center.x - dx, center.x + dx)]
10874 } else {
10875 vec![]
10876 }
10877 }
10878 ShapeBoundary::Ellipse { center, radii } => {
10879 let line_center_y = y + line_height / 2.0;
10880 let dy = line_center_y - center.y;
10881 if dy.abs() <= radii.height {
10882 let y_term = dy / radii.height;
10884 let x_term_squared = 1.0 - y_term.powi(2);
10885 if x_term_squared >= 0.0 {
10886 let dx = radii.width * x_term_squared.sqrt();
10887 vec![(center.x - dx, center.x + dx)]
10888 } else {
10889 vec![]
10890 }
10891 } else {
10892 vec![]
10893 }
10894 }
10895 ShapeBoundary::Polygon { points } => {
10896 let segments = polygon_line_intersection(points, y, line_height);
10897 segments
10898 .iter()
10899 .map(|s| (s.start_x, s.start_x + s.width))
10900 .collect()
10901 }
10902 ShapeBoundary::Path { segments } => {
10907 path_segments_line_intersection(segments, y, line_height)
10908 }
10909 }
10910}
10911
10912fn merge_segments(mut segments: Vec<LineSegment>) -> Vec<LineSegment> {
10914 if segments.len() <= 1 {
10915 return segments;
10916 }
10917 segments.sort_by(|a, b| a.start_x.partial_cmp(&b.start_x).unwrap_or(Ordering::Equal));
10918 let mut merged = vec![segments[0]];
10919 for next_seg in segments.iter().skip(1) {
10920 let last = merged.last_mut().unwrap();
10921 if next_seg.start_x <= last.start_x + last.width {
10922 let new_width = (next_seg.start_x + next_seg.width) - last.start_x;
10923 last.width = last.width.max(new_width);
10924 } else {
10925 merged.push(*next_seg);
10926 }
10927 }
10928 merged
10929}
10930
10931#[allow(clippy::suboptimal_flops)] fn polygon_line_intersection(
10934 points: &[Point],
10935 y: f32,
10936 line_height: f32,
10937) -> Vec<LineSegment> {
10938 if points.len() < 3 {
10939 return vec![];
10940 }
10941
10942 let line_center_y = y + line_height / 2.0;
10943 let mut intersections = Vec::new();
10944
10945 for i in 0..points.len() {
10947 let p1 = points[i];
10948 let p2 = points[(i + 1) % points.len()];
10949
10950 if (p2.y - p1.y).abs() < f32::EPSILON {
10952 continue;
10953 }
10954
10955 let crosses = (p1.y <= line_center_y && p2.y > line_center_y)
10957 || (p1.y > line_center_y && p2.y <= line_center_y);
10958
10959 if crosses {
10960 let t = (line_center_y - p1.y) / (p2.y - p1.y);
10962 let x = p1.x + t * (p2.x - p1.x);
10963 intersections.push(x);
10964 }
10965 }
10966
10967 intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
10969
10970 let mut segments = Vec::new();
10972 for chunk in intersections.chunks_exact(2) {
10973 let start_x = chunk[0];
10974 let end_x = chunk[1];
10975 if end_x > start_x {
10976 segments.push(LineSegment {
10977 start_x,
10978 width: end_x - start_x,
10979 priority: 0,
10980 });
10981 }
10982 }
10983
10984 segments
10985}
10986
10987#[cfg(feature = "text_layout_hyphenation")]
10991fn get_hyphenator(language: HyphenationLanguage) -> Result<Standard, LayoutError> {
10992 Standard::from_embedded(language).map_err(|e| LayoutError::HyphenationError(e.to_string()))
10993}
10994
10995#[cfg(not(feature = "text_layout_hyphenation"))]
10997fn get_hyphenator(_language: Language) -> Result<Standard, LayoutError> {
10998 Err(LayoutError::HyphenationError("Hyphenation feature not enabled".to_string()))
10999}
11000
11001const fn is_break_suppressing_control(ch: char) -> bool {
11004 matches!(ch,
11005 '\u{200D}' | '\u{2060}' | '\u{FEFF}' )
11009}
11010
11011const fn is_break_forcing_control(ch: char) -> bool {
11012 matches!(ch,
11013 '\u{200B}' | '\u{2028}' | '\u{2029}' )
11017}
11018
11019const fn is_cjk_character(ch: char) -> bool {
11022 let cp = ch as u32;
11023 matches!(cp,
11024 0x4E00..=0x9FFF |
11026 0x3400..=0x4DBF |
11028 0x20000..=0x2A6DF |
11030 0xF900..=0xFAFF |
11032 0x3040..=0x309F |
11034 0x30A0..=0x30FF |
11036 0x31F0..=0x31FF |
11038 0x3000..=0x303F |
11040 0xFF00..=0xFFEF |
11042 0xAC00..=0xD7AF
11044 )
11045}
11046
11047fn is_cjk_cluster(cluster: &ShapedCluster) -> bool {
11049 cluster.text.chars().any(is_cjk_character)
11050}
11051
11052fn is_break_opportunity_with_word_break(item: &ShapedItem, word_break: WordBreak, hyphens: Hyphens) -> bool {
11065 if let ShapedItem::Cluster(c) = item {
11070 if c.text
11071 .chars()
11072 .any(|ch| matches!(ch, '\u{00A0}' | '\u{202F}' | '\u{2060}' | '\u{FEFF}'))
11073 {
11074 return false;
11075 }
11076 }
11077 if is_word_separator(item) {
11079 return true;
11080 }
11081 if let ShapedItem::Break { .. } = item {
11082 return true;
11083 }
11084 if is_zero_width_space(item) {
11090 return true;
11091 }
11092 if hyphens != Hyphens::None {
11094 if let ShapedItem::Cluster(c) = item {
11095 if c.text.starts_with('\u{00AD}') {
11096 return true;
11097 }
11098 }
11099 }
11100
11101 if let ShapedItem::Cluster(c) = item {
11108 if c.text.ends_with('\u{002D}') || c.text.ends_with('\u{2010}') || c.text.ends_with('\u{002F}') {
11109 return true;
11110 }
11111 }
11112
11113 match word_break {
11115 WordBreak::Normal => {
11116 if let ShapedItem::Cluster(c) = item {
11118 if is_cjk_cluster(c) {
11119 return true;
11120 }
11121 }
11122 false
11123 }
11124 WordBreak::BreakAll => {
11125 if let ShapedItem::Cluster(_) = item {
11127 return true;
11128 }
11129 false
11130 }
11131 WordBreak::KeepAll => {
11132 false
11135 }
11136 }
11137}
11138
11139#[allow(clippy::match_same_arms)] const fn is_cjk_break_allowed_by_strictness(
11150 ch: char,
11151 _prev_ch: Option<char>,
11152 strictness: LineBreakStrictness,
11153) -> bool {
11154 match strictness {
11155 LineBreakStrictness::Anywhere => true,
11156 LineBreakStrictness::Loose => {
11157 true
11160 }
11161 LineBreakStrictness::Normal | LineBreakStrictness::Auto => {
11162 match ch {
11166 '\u{2010}' | '\u{2013}' => false, _ => true,
11168 }
11169 }
11170 LineBreakStrictness::Strict => {
11171 match ch {
11176 '\u{301C}' | '\u{30A0}' => false, '\u{2010}' | '\u{2013}' => false, c if is_small_kana(c) => false,
11179 _ => true,
11180 }
11181 }
11182 }
11183}
11184
11185const fn is_small_kana(ch: char) -> bool {
11188 matches!(ch,
11189 '\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}' )
11215}
11216
11217fn is_break_opportunity(item: &ShapedItem) -> bool {
11220 if matches!(item, ShapedItem::Object { .. } | ShapedItem::CombinedBlock { .. }) {
11223 return true;
11224 }
11225 if let ShapedItem::Cluster(c) = item {
11229 if c.text.contains('\u{200B}') {
11231 return true;
11232 }
11233 if c.text.chars().any(is_break_forcing_control) {
11235 return true;
11236 }
11237 if c.text.chars().any(|ch| matches!(ch, '\u{2060}' | '\u{200D}' | '\u{00A0}')) {
11239 return false;
11240 }
11241 if c.text.ends_with('\u{002D}') || c.text.ends_with('\u{2010}') {
11245 return true;
11246 }
11247 }
11248 is_break_opportunity_with_word_break(item, WordBreak::Normal, Hyphens::Manual)
11249}
11250
11251#[derive(Debug, Clone)]
11256pub struct BreakCursor<'a> {
11257 pub items: &'a [ShapedItem],
11259 pub next_item_index: usize,
11261 pub partial_remainder: Vec<ShapedItem>,
11264 pub word_break: WordBreak,
11266 pub hyphens: Hyphens,
11267 pub line_break: LineBreakStrictness,
11268}
11269
11270impl<'a> BreakCursor<'a> {
11271 #[must_use] pub fn new(items: &'a [ShapedItem]) -> Self {
11272 Self {
11273 items,
11274 next_item_index: 0,
11275 partial_remainder: Vec::new(),
11276 word_break: WordBreak::Normal,
11277 hyphens: Hyphens::default(),
11278 line_break: LineBreakStrictness::default(),
11279 }
11280 }
11281
11282 #[must_use] pub fn with_word_break(items: &'a [ShapedItem], word_break: WordBreak) -> Self {
11283 Self {
11284 items,
11285 next_item_index: 0,
11286 partial_remainder: Vec::new(),
11287 word_break,
11288 hyphens: Hyphens::default(),
11289 line_break: LineBreakStrictness::default(),
11290 }
11291 }
11292
11293 #[must_use] pub const fn is_at_start(&self) -> bool {
11295 self.next_item_index == 0 && self.partial_remainder.is_empty()
11296 }
11297
11298 pub fn drain_remaining(&mut self) -> Vec<ShapedItem> {
11300 let mut remaining = std::mem::take(&mut self.partial_remainder);
11301 if self.next_item_index < self.items.len() {
11302 remaining.extend_from_slice(&self.items[self.next_item_index..]);
11303 }
11304 self.next_item_index = self.items.len();
11305 remaining
11306 }
11307
11308 #[must_use] pub const fn is_done(&self) -> bool {
11310 self.next_item_index >= self.items.len() && self.partial_remainder.is_empty()
11311 }
11312
11313 pub fn consume(&mut self, count: usize) {
11315 if count == 0 {
11316 return;
11317 }
11318
11319 let remainder_len = self.partial_remainder.len();
11320 if count <= remainder_len {
11321 self.partial_remainder.drain(..count);
11323 } else {
11324 let from_main_list = count - remainder_len;
11326 self.partial_remainder.clear();
11327 self.next_item_index += from_main_list;
11328 }
11329 }
11330
11331 pub fn peek_next_unit(&self) -> Vec<ShapedItem> {
11338 let mut unit = Vec::new();
11339 let mut source_items = self.partial_remainder.clone();
11340 source_items.extend_from_slice(&self.items[self.next_item_index..]);
11341
11342 if source_items.is_empty() {
11343 return unit;
11344 }
11345
11346 if is_break_opportunity_with_word_break(&source_items[0], self.word_break, self.hyphens) {
11348 unit.push(source_items[0].clone());
11349 return unit;
11350 }
11351
11352 let mut suppress_next_break = false;
11359 for (i, item) in source_items.iter().enumerate() {
11360 let starts_with_suppress = if let ShapedItem::Cluster(c) = item {
11363 c.text.chars().next().is_some_and(is_break_suppressing_control)
11364 } else {
11365 false
11366 };
11367 let cjk_strictness_suppressed = if let ShapedItem::Cluster(c) = item {
11369 c.text.chars().next().is_some_and(|ch| {
11370 !is_cjk_break_allowed_by_strictness(ch, None, self.line_break)
11371 })
11372 } else {
11373 false
11374 };
11375 if i > 0 && !suppress_next_break && !starts_with_suppress && !cjk_strictness_suppressed && is_break_opportunity_with_word_break(item, self.word_break, self.hyphens) {
11376 break;
11377 }
11378 suppress_next_break = false;
11379 unit.push(item.clone());
11380
11381 if let ShapedItem::Cluster(c) = item {
11383 if let Some(last_ch) = c.text.chars().last() {
11384 if is_break_suppressing_control(last_ch) {
11385 suppress_next_break = true;
11386 }
11387 }
11388 }
11389
11390 if self.word_break == WordBreak::BreakAll {
11392 if let ShapedItem::Cluster(_) = item {
11393 break;
11394 }
11395 }
11396 }
11397 unit
11398 }
11399
11400 #[must_use] pub fn peek_next_single_item(&self) -> Vec<ShapedItem> {
11401 if !self.partial_remainder.is_empty() {
11402 return vec![self.partial_remainder[0].clone()];
11403 }
11404 if self.next_item_index < self.items.len() {
11405 return vec![self.items[self.next_item_index].clone()];
11406 }
11407 Vec::new()
11408 }
11409}
11410
11411struct HyphenationResult {
11413 line_part: Vec<ShapedItem>,
11415 remainder_part: Vec<ShapedItem>,
11417}
11418
11419fn perform_bidi_analysis<'a>(
11420 styled_runs: &'a [TextRunInfo<'_>],
11421 full_text: &'a str,
11422 force_lang: Option<Language>,
11423) -> (Vec<VisualRun<'a>>, BidiDirection) {
11424 if full_text.is_empty() {
11425 return (Vec::new(), BidiDirection::Ltr);
11426 }
11427
11428 let bidi_info = BidiInfo::new(full_text, None);
11429 let para = &bidi_info.paragraphs[0];
11430 let base_direction = if para.level.is_rtl() {
11431 BidiDirection::Rtl
11432 } else {
11433 BidiDirection::Ltr
11434 };
11435
11436 let mut byte_to_run_index: Vec<usize> = vec![0; full_text.len()];
11438 for (run_idx, run) in styled_runs.iter().enumerate() {
11439 let start = run.logical_start;
11440 let end = start + run.text.len();
11441 for slot in &mut byte_to_run_index[start..end] {
11442 *slot = run_idx;
11443 }
11444 }
11445
11446 let mut final_visual_runs = Vec::new();
11447 let (levels, visual_run_ranges) = bidi_info.visual_runs(para, para.range.clone());
11448
11449 for range in visual_run_ranges {
11450 let bidi_level = levels[range.start];
11451 let mut sub_run_start = range.start;
11452
11453 for i in (range.start + 1)..range.end {
11455 if byte_to_run_index[i] != byte_to_run_index[sub_run_start] {
11456 let original_run_idx = byte_to_run_index[sub_run_start];
11458 let script = crate::text3::script::detect_script(&full_text[sub_run_start..i])
11459 .unwrap_or(Script::Latin);
11460 final_visual_runs.push(VisualRun {
11461 text_slice: &full_text[sub_run_start..i],
11462 style: styled_runs[original_run_idx].style.clone(),
11463 logical_start_byte: sub_run_start,
11464 bidi_level: BidiLevel::new(bidi_level.number()),
11465 language: force_lang.unwrap_or_else(|| {
11466 script_to_language(
11467 script,
11468 &full_text[sub_run_start..i],
11469 )
11470 }),
11471 script,
11472 });
11473 sub_run_start = i;
11475 }
11476 }
11477
11478 let original_run_idx = byte_to_run_index[sub_run_start];
11480 let script = crate::text3::script::detect_script(&full_text[sub_run_start..range.end])
11481 .unwrap_or(Script::Latin);
11482
11483 final_visual_runs.push(VisualRun {
11484 text_slice: &full_text[sub_run_start..range.end],
11485 style: styled_runs[original_run_idx].style.clone(),
11486 logical_start_byte: sub_run_start,
11487 bidi_level: BidiLevel::new(bidi_level.number()),
11488 script,
11489 language: force_lang.unwrap_or_else(|| {
11490 script_to_language(
11491 script,
11492 &full_text[sub_run_start..range.end],
11493 )
11494 }),
11495 });
11496 }
11497
11498 (final_visual_runs, base_direction)
11499}
11500
11501const fn get_justification_priority(class: CharacterClass) -> u8 {
11502 match class {
11503 CharacterClass::Space => 0,
11504 CharacterClass::Punctuation => 64,
11505 CharacterClass::Ideograph => 128,
11506 CharacterClass::Letter => 192,
11507 CharacterClass::Symbol => 224,
11508 CharacterClass::Combining => 255,
11509 }
11510}
11511
11512#[cfg(test)]
11513mod shape_outside_and_ruby_tests {
11514 use super::*;
11515 use azul_css::shape::{CssShape, ShapePath};
11516
11517 fn path_shape(d: &str) -> CssShape {
11518 CssShape::Path(ShapePath {
11519 data: d.into(),
11520 })
11521 }
11522
11523 #[test]
11526 fn css_path_shape_builds_path_boundary_not_rect_fallback() {
11527 let shape = path_shape("M 0 0 L 100 0 L 0 100 Z");
11529 let rbox = Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 };
11530 let boundary = ShapeBoundary::from_css_shape(&shape, rbox, &mut None);
11531 match boundary {
11532 ShapeBoundary::Path { segments } => {
11533 assert!(!segments.is_empty(), "path() must flatten to real segments");
11534 assert!(matches!(segments[0], PathSegment::MoveTo(_)));
11535 assert!(segments.iter().any(|s| matches!(s, PathSegment::Close)));
11536 }
11537 other => panic!("expected ShapeBoundary::Path, got {other:?}"),
11538 }
11539 }
11540
11541 #[test]
11542 fn empty_or_garbage_path_falls_back_to_rectangle() {
11543 let rbox = Rect { x: 0.0, y: 0.0, width: 50.0, height: 50.0 };
11544 let boundary = ShapeBoundary::from_css_shape(&path_shape(" "), rbox, &mut None);
11545 assert!(matches!(boundary, ShapeBoundary::Rectangle(_)),
11546 "unparseable path() should fall back to the reference rectangle");
11547 }
11548
11549 #[test]
11550 fn path_triangle_narrows_line_box_per_scanline() {
11551 let shape = path_shape("M 0 0 L 100 0 L 0 100 Z");
11556 let rbox = Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 };
11557 let boundary = ShapeBoundary::from_css_shape(&shape, rbox, &mut None);
11558
11559 let spans_top = get_shape_horizontal_spans(&boundary, 10.0, 1.0);
11560 let spans_bot = get_shape_horizontal_spans(&boundary, 80.0, 1.0);
11561
11562 assert_eq!(spans_top.len(), 1, "single span expected near the top");
11563 assert_eq!(spans_bot.len(), 1, "single span expected near the bottom");
11564
11565 let width_top = spans_top[0].1 - spans_top[0].0;
11566 let width_bot = spans_bot[0].1 - spans_bot[0].0;
11567
11568 assert!((width_top - 89.5).abs() < 1.5, "top width {width_top} != ~89.5");
11570 assert!((width_bot - 19.5).abs() < 1.5, "bottom width {width_bot} != ~19.5");
11571 assert!(width_top > width_bot,
11572 "path() exclusion band must narrow with y ({width_top} !> {width_bot})");
11573
11574 assert!(width_bot < 50.0, "rect fallback would give full width here");
11577 }
11578
11579 #[test]
11580 fn path_with_hole_carves_out_interior_via_even_odd() {
11581 let shape = path_shape(
11584 "M 0 0 L 100 0 L 100 100 L 0 100 Z \
11585 M 30 30 L 30 70 L 70 70 L 70 30 Z",
11586 );
11587 let rbox = Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 };
11588 let boundary = ShapeBoundary::from_css_shape(&shape, rbox, &mut None);
11589 let spans = get_shape_horizontal_spans(&boundary, 50.0, 1.0);
11590 assert_eq!(spans.len(), 2, "hole should split the band into two spans: {spans:?}");
11591 }
11592
11593 #[test]
11596 #[allow(clippy::float_cmp)] fn ruby_annotation_font_scale_is_real_not_06_fudge() {
11598 let base_font_size = 20.0_f32;
11601 let annotation_font_size = base_font_size * RUBY_ANNOTATION_FONT_SCALE;
11602 assert_eq!(annotation_font_size, 10.0);
11603 assert!((RUBY_ANNOTATION_FONT_SCALE - 0.6).abs() > f32::EPSILON,
11604 "annotation scale must not be the old 0.6 magic ratio");
11605 }
11606
11607 #[test]
11608 #[allow(clippy::float_cmp)] fn ruby_box_reserves_max_width_and_stacks_annotation_above_base() {
11610 let (w, h) = ruby_reserved_box(80.0, 30.0, 24.0, 12.0);
11612 assert_eq!(w, 80.0, "reserved width is the wider of base/annotation");
11613 assert_eq!(h, 36.0, "block-size = base line + annotation line");
11616 assert!(h > 24.0, "ruby box must reserve extra vertical space for the annotation");
11617
11618 let (w2, _) = ruby_reserved_box(20.0, 50.0, 24.0, 12.0);
11620 assert_eq!(w2, 50.0, "a long annotation widens the reserved box");
11621 }
11622}
11623
11624#[cfg(test)]
11632#[allow(
11633 clippy::float_cmp,
11634 clippy::too_many_lines,
11635 clippy::unreadable_literal,
11636 clippy::cast_precision_loss,
11637 clippy::similar_names
11638)]
11639mod autotest_generated {
11640 use super::*;
11641
11642 fn metrics(upem: u16, ascent: f32, descent: f32, line_gap: f32) -> LayoutFontMetrics {
11647 LayoutFontMetrics {
11648 ascent,
11649 descent,
11650 line_gap,
11651 units_per_em: upem,
11652 x_height: None,
11653 cap_height: None,
11654 }
11655 }
11656
11657 fn std_metrics() -> LayoutFontMetrics {
11659 metrics(1000, 800.0, -200.0, 0.0)
11660 }
11661
11662 fn style() -> Arc<StyleProperties> {
11663 Arc::new(StyleProperties::default())
11664 }
11665
11666 fn styled(f: impl FnOnce(&mut StyleProperties)) -> Arc<StyleProperties> {
11667 let mut s = StyleProperties::default();
11668 f(&mut s);
11669 Arc::new(s)
11670 }
11671
11672 const fn ci(run: u32, item: u32) -> ContentIndex {
11673 ContentIndex {
11674 run_index: run,
11675 item_index: item,
11676 }
11677 }
11678
11679 const fn gid(run: u32, byte: u32) -> GraphemeClusterId {
11680 GraphemeClusterId {
11681 source_run: run,
11682 start_byte_in_run: byte,
11683 }
11684 }
11685
11686 fn shaped_glyph(st: Arc<StyleProperties>, fm: LayoutFontMetrics, advance: f32) -> ShapedGlyph {
11687 ShapedGlyph {
11688 kind: GlyphKind::Character,
11689 glyph_id: 42,
11690 cluster_offset: 0,
11691 advance,
11692 kerning: 0.0,
11693 offset: Point { x: 0.0, y: 0.0 },
11694 vertical_advance: advance,
11695 vertical_offset: Point { x: 0.0, y: 0.0 },
11696 script: Script::Latin,
11697 style: st,
11698 font_hash: 0xABCD_u64,
11699 font_metrics: fm,
11700 }
11701 }
11702
11703 fn make_cluster(
11704 text: &str,
11705 advance: f32,
11706 st: Arc<StyleProperties>,
11707 glyphs: ShapedGlyphVec,
11708 id: GraphemeClusterId,
11709 ) -> ShapedItem {
11710 ShapedItem::Cluster(ShapedCluster {
11711 text: text.to_string(),
11712 source_cluster_id: id,
11713 source_content_index: ci(id.source_run, id.start_byte_in_run),
11714 source_node_id: None,
11715 glyphs,
11716 advance,
11717 direction: BidiDirection::Ltr,
11718 style: st,
11719 marker_position_outside: None,
11720 is_first_fragment: true,
11721 is_last_fragment: true,
11722 })
11723 }
11724
11725 fn cl(text: &str, advance: f32) -> ShapedItem {
11727 let st = style();
11728 let g = shaped_glyph(st.clone(), std_metrics(), advance);
11729 make_cluster(text, advance, st, smallvec![g], gid(0, 0))
11730 }
11731
11732 fn cl_at(text: &str, advance: f32, run: u32, byte: u32) -> ShapedItem {
11734 let st = style();
11735 let g = shaped_glyph(st.clone(), std_metrics(), advance);
11736 make_cluster(text, advance, st, smallvec![g], gid(run, byte))
11737 }
11738
11739 fn cl_styled(text: &str, advance: f32, st: Arc<StyleProperties>) -> ShapedItem {
11741 let g = shaped_glyph(st.clone(), std_metrics(), advance);
11742 make_cluster(text, advance, st, smallvec![g], gid(0, 0))
11743 }
11744
11745 fn cl_no_glyphs(text: &str, advance: f32) -> ShapedItem {
11747 make_cluster(text, advance, style(), ShapedGlyphVec::new(), gid(0, 0))
11748 }
11749
11750 fn obj(width: f32, height: f32, baseline_offset: f32) -> ShapedItem {
11751 ShapedItem::Object {
11752 source: ci(0, 0),
11753 bounds: Rect {
11754 x: 0.0,
11755 y: 0.0,
11756 width,
11757 height,
11758 },
11759 baseline_offset,
11760 content: InlineContent::Space(InlineSpace {
11761 width,
11762 is_breaking: false,
11763 is_stretchy: false,
11764 }),
11765 }
11766 }
11767
11768 fn brk() -> ShapedItem {
11769 ShapedItem::Break {
11770 source: ci(0, 0),
11771 break_info: InlineBreak {
11772 break_type: BreakType::Hard,
11773 clear: ClearType::None,
11774 content_index: 0,
11775 },
11776 }
11777 }
11778
11779 fn tab(width: f32, height: f32) -> ShapedItem {
11780 ShapedItem::Tab {
11781 source: ci(0, 0),
11782 bounds: Rect {
11783 x: 0.0,
11784 y: 0.0,
11785 width,
11786 height,
11787 },
11788 }
11789 }
11790
11791 fn pos(item: ShapedItem, x: f32, y: f32, line_index: usize) -> PositionedItem {
11792 PositionedItem {
11793 item,
11794 position: Point { x, y },
11795 line_index,
11796 }
11797 }
11798
11799 fn text_content(t: &str, st: Arc<StyleProperties>) -> InlineContent {
11800 InlineContent::Text(StyledRun {
11801 text: t.to_string(),
11802 style: st,
11803 logical_start_byte: 0,
11804 source_node_id: None,
11805 })
11806 }
11807
11808 fn sel(family: &str) -> FontSelector {
11809 FontSelector {
11810 family: family.to_string(),
11811 ..FontSelector::default()
11812 }
11813 }
11814
11815 #[derive(Debug, Clone)]
11818 struct TestFont {
11819 hash: u64,
11820 }
11821
11822 impl ShallowClone for TestFont {
11823 fn shallow_clone(&self) -> Self {
11824 self.clone()
11825 }
11826 }
11827
11828 impl ParsedFontTrait for TestFont {
11829 fn shape_text(
11830 &self,
11831 _text: &str,
11832 _script: Script,
11833 _language: Language,
11834 _direction: BidiDirection,
11835 _style: &StyleProperties,
11836 ) -> Result<Vec<Glyph>, LayoutError> {
11837 Ok(Vec::new())
11838 }
11839 fn get_hash(&self) -> u64 {
11840 self.hash
11841 }
11842 fn get_glyph_size(&self, _glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
11843 Some(LogicalSize {
11844 width: font_size,
11845 height: font_size,
11846 })
11847 }
11848 fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
11849 Some((1, font_size * 0.3))
11850 }
11851 fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
11852 Some((2, font_size * 0.2))
11853 }
11854 fn has_glyph(&self, _codepoint: u32) -> bool {
11855 true
11856 }
11857 fn get_vertical_metrics(&self, _glyph_id: u16) -> Option<VerticalMetrics> {
11858 None
11859 }
11860 fn get_font_metrics(&self) -> LayoutFontMetrics {
11861 std_metrics()
11862 }
11863 fn num_glyphs(&self) -> u16 {
11864 10
11865 }
11866 fn get_space_width(&self) -> Option<usize> {
11867 Some(500)
11868 }
11869 }
11870
11871 fn hash_of<T: Hash>(v: &T) -> u64 {
11872 let mut h = DefaultHasher::new();
11873 v.hash(&mut h);
11874 h.finish()
11875 }
11876
11877 #[track_caller]
11880 fn approx(actual: f32, expected: f32) {
11881 assert!(
11882 (actual - expected).abs() < 1e-4,
11883 "expected ~{expected}, got {actual}"
11884 );
11885 }
11886
11887 #[test]
11892 fn ruby_reserved_box_zero_and_negative_are_deterministic() {
11893 assert_eq!(ruby_reserved_box(0.0, 0.0, 0.0, 0.0), (0.0, 0.0));
11894 let (w, h) = ruby_reserved_box(-10.0, -4.0, -3.0, -2.0);
11896 assert_eq!(w, -4.0);
11897 assert_eq!(h, -5.0);
11898 }
11899
11900 #[test]
11901 fn ruby_reserved_box_nan_width_is_ignored_by_max_but_poisons_height() {
11902 let (w, h) = ruby_reserved_box(f32::NAN, 30.0, 24.0, f32::NAN);
11905 assert_eq!(w, 30.0, "f32::max discards the NaN operand");
11906 assert!(h.is_nan(), "but the additive block-size does propagate NaN");
11907 }
11908
11909 #[test]
11910 fn ruby_reserved_box_infinities_do_not_panic() {
11911 let (w, h) = ruby_reserved_box(f32::INFINITY, 10.0, f32::INFINITY, f32::NEG_INFINITY);
11912 assert!(w.is_infinite() && w.is_sign_positive());
11913 assert!(h.is_nan(), "inf + -inf is NaN, not a panic");
11914 }
11915
11916 #[test]
11917 fn ruby_reserved_box_saturates_to_infinity_at_f32_max() {
11918 let (w, h) = ruby_reserved_box(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
11919 assert_eq!(w, f32::MAX);
11920 assert!(h.is_infinite(), "f32 addition saturates, it does not panic");
11921 }
11922
11923 #[test]
11928 fn line_height_px_ignores_every_font_metric() {
11929 let lh = LineHeight::Px(7.5);
11930 assert_eq!(lh.resolve(16.0, 800.0, -200.0, 0.0, 1000), 7.5);
11931 assert_eq!(lh.resolve(f32::NAN, f32::NAN, f32::NAN, f32::NAN, 0), 7.5);
11933 }
11934
11935 #[test]
11936 fn line_height_normal_zero_upem_falls_back_to_1_2_em() {
11937 let lh = LineHeight::Normal;
11938 assert_eq!(lh.resolve(16.0, 800.0, -200.0, 0.0, 0), 19.2);
11939 assert_eq!(lh.resolve(0.0, 800.0, -200.0, 0.0, 0), 0.0);
11940 }
11941
11942 #[test]
11943 fn line_height_normal_scales_ascent_minus_descent_plus_gap() {
11944 approx(LineHeight::Normal.resolve(16.0, 800.0, -200.0, 0.0, 1000), 16.0);
11946 approx(
11948 LineHeight::Normal.resolve(16.0, 800.0, -200.0, 250.0, 1000),
11949 20.0,
11950 );
11951 approx(LineHeight::Normal.resolve(16.0, 800.0, 200.0, 0.0, 1000), 9.6);
11954 }
11955
11956 #[test]
11957 fn line_height_normal_at_u16_max_upem_does_not_panic() {
11958 let v = LineHeight::Normal.resolve(16.0, 800.0, -200.0, 0.0, u16::MAX);
11959 assert!(v.is_finite() && v > 0.0, "got {v}");
11960 assert!(v < 1.0, "a 65535-upem font must produce a tiny scale, got {v}");
11961 }
11962
11963 #[test]
11964 fn line_height_normal_nan_and_inf_inputs_are_defined_not_panics() {
11965 assert!(LineHeight::Normal
11966 .resolve(f32::NAN, 800.0, -200.0, 0.0, 1000)
11967 .is_nan());
11968 assert!(LineHeight::Normal
11969 .resolve(f32::INFINITY, 800.0, -200.0, 0.0, 1000)
11970 .is_infinite());
11971 assert!(LineHeight::Normal
11973 .resolve(16.0, f32::INFINITY, f32::INFINITY, 0.0, 1000)
11974 .is_nan());
11975 }
11976
11977 #[test]
11978 fn line_height_resolve_with_metrics_matches_resolve() {
11979 let fm = metrics(2048, 1600.0, -400.0, 100.0);
11980 let lh = LineHeight::Normal;
11981 assert_eq!(
11982 lh.resolve_with_metrics(24.0, &fm),
11983 lh.resolve(24.0, fm.ascent, fm.descent, fm.line_gap, fm.units_per_em)
11984 );
11985 assert_eq!(LineHeight::Px(3.0).resolve_with_metrics(24.0, &fm), 3.0);
11987 }
11988
11989 #[test]
11990 fn line_height_px_nan_is_self_equal_under_the_manual_partialeq() {
11991 assert_eq!(LineHeight::Px(f32::NAN), LineHeight::Px(f32::NAN));
11994 assert_eq!(
11995 hash_of(&LineHeight::Px(f32::NAN)),
11996 hash_of(&LineHeight::Px(f32::NAN))
11997 );
11998 assert_ne!(LineHeight::Normal, LineHeight::Px(0.0));
11999 }
12000
12001 #[test]
12006 fn available_space_definite_and_indefinite_are_exact_complements() {
12007 for v in [
12008 AvailableSpace::Definite(0.0),
12009 AvailableSpace::Definite(-1.0),
12010 AvailableSpace::Definite(f32::NAN),
12011 AvailableSpace::MinContent,
12012 AvailableSpace::MaxContent,
12013 ] {
12014 assert_ne!(v.is_definite(), v.is_indefinite(), "{v:?}");
12015 }
12016 assert!(AvailableSpace::Definite(f32::NAN).is_definite());
12017 assert!(AvailableSpace::default().is_indefinite());
12018 assert_eq!(AvailableSpace::default(), AvailableSpace::MaxContent);
12019 }
12020
12021 #[test]
12022 fn available_space_unwrap_or_returns_definite_even_when_nan_or_inf() {
12023 assert_eq!(AvailableSpace::Definite(0.0).unwrap_or(99.0), 0.0);
12024 assert_eq!(AvailableSpace::Definite(-5.0).unwrap_or(99.0), -5.0);
12025 assert!(AvailableSpace::Definite(f32::NAN).unwrap_or(99.0).is_nan());
12026 assert!(AvailableSpace::Definite(f32::INFINITY)
12027 .unwrap_or(99.0)
12028 .is_infinite());
12029 assert_eq!(AvailableSpace::MinContent.unwrap_or(99.0), 99.0);
12031 assert_eq!(AvailableSpace::MaxContent.unwrap_or(-0.0), -0.0);
12032 assert!(AvailableSpace::MaxContent.unwrap_or(f32::NAN).is_nan());
12033 }
12034
12035 #[test]
12036 fn available_space_to_f32_for_layout_uses_half_max_for_both_intrinsic_modes() {
12037 assert_eq!(AvailableSpace::MinContent.to_f32_for_layout(), f32::MAX / 2.0);
12038 assert_eq!(AvailableSpace::MaxContent.to_f32_for_layout(), f32::MAX / 2.0);
12039 assert_eq!(AvailableSpace::Definite(12.5).to_f32_for_layout(), 12.5);
12040 assert!(AvailableSpace::Definite(f32::NAN)
12041 .to_f32_for_layout()
12042 .is_nan());
12043 }
12044
12045 #[test]
12046 fn available_space_from_f32_sentinels() {
12047 assert_eq!(AvailableSpace::from_f32(f32::INFINITY), AvailableSpace::MaxContent);
12048 assert_eq!(AvailableSpace::from_f32(f32::MAX), AvailableSpace::MaxContent);
12049 assert_eq!(
12051 AvailableSpace::from_f32(f32::MAX / 2.0),
12052 AvailableSpace::MaxContent
12053 );
12054 assert_eq!(AvailableSpace::from_f32(0.0), AvailableSpace::MinContent);
12055 assert_eq!(AvailableSpace::from_f32(-0.0), AvailableSpace::MinContent);
12056 assert_eq!(AvailableSpace::from_f32(-1.0), AvailableSpace::MinContent);
12057 assert_eq!(AvailableSpace::from_f32(100.0), AvailableSpace::Definite(100.0));
12058 }
12059
12060 #[test]
12061 fn available_space_from_f32_negative_infinity_becomes_max_content() {
12062 assert_eq!(
12066 AvailableSpace::from_f32(f32::NEG_INFINITY),
12067 AvailableSpace::MaxContent
12068 );
12069 }
12070
12071 #[test]
12072 fn available_space_from_f32_nan_falls_through_to_definite_nan() {
12073 let got = AvailableSpace::from_f32(f32::NAN);
12076 match got {
12077 AvailableSpace::Definite(v) => assert!(v.is_nan(), "expected Definite(NaN)"),
12078 other => panic!("NaN should fall through to Definite, got {other:?}"),
12079 }
12080 assert_ne!(got, AvailableSpace::from_f32(f32::NAN));
12082 }
12083
12084 #[test]
12085 fn available_space_hash_eq_contract_holds_for_signed_zero() {
12086 assert_eq!(
12088 AvailableSpace::Definite(0.0),
12089 AvailableSpace::Definite(-0.0)
12090 );
12091 assert_eq!(
12092 hash_of(&AvailableSpace::Definite(0.0)),
12093 hash_of(&AvailableSpace::Definite(-0.0))
12094 );
12095 assert_ne!(
12097 hash_of(&AvailableSpace::Definite(100.1)),
12098 hash_of(&AvailableSpace::Definite(100.4))
12099 );
12100 assert_ne!(
12101 hash_of(&AvailableSpace::MinContent),
12102 hash_of(&AvailableSpace::MaxContent)
12103 );
12104 }
12105
12106 #[test]
12111 fn font_chain_key_from_empty_selectors_defaults_to_serif() {
12112 let k = FontChainKey::from_selectors(&[]);
12113 assert_eq!(k.font_families, vec!["serif".to_string()]);
12114 assert_eq!(k.weight, FcWeight::Normal);
12115 assert!(!k.italic && !k.oblique);
12116 }
12117
12118 #[test]
12119 fn font_chain_key_dedups_first_wins_and_skips_empty_families() {
12120 let stack = [sel("Arial"), sel("Times"), sel("Arial"), sel("")];
12121 let k = FontChainKey::from_selectors(&stack);
12122 assert_eq!(
12123 k.font_families,
12124 vec!["Arial".to_string(), "Times".to_string()],
12125 "duplicate families must collapse first-wins, empty names dropped"
12126 );
12127 }
12128
12129 #[test]
12130 fn font_chain_key_all_empty_families_still_yields_serif() {
12131 let stack = [sel(""), sel(""), sel("")];
12132 let k = FontChainKey::from_selectors(&stack);
12133 assert_eq!(k.font_families, vec!["serif".to_string()]);
12134 }
12135
12136 #[test]
12137 fn font_chain_key_weight_and_style_come_from_the_first_selector_even_if_it_is_dropped() {
12138 let mut first = sel("");
12141 first.style = FontStyle::Italic;
12142 first.weight = FcWeight::Bold;
12143 let stack = [first, sel("Arial")];
12144 let k = FontChainKey::from_selectors(&stack);
12145 assert_eq!(k.font_families, vec!["Arial".to_string()]);
12146 assert_eq!(k.weight, FcWeight::Bold);
12147 assert!(k.italic, "italic taken from the dropped first selector");
12148 assert!(!k.oblique);
12149 }
12150
12151 #[test]
12152 fn font_chain_key_oblique_is_exclusive_of_italic() {
12153 let mut s = sel("Arial");
12154 s.style = FontStyle::Oblique;
12155 let k = FontChainKey::from_selectors(&[s]);
12156 assert!(k.oblique && !k.italic);
12157 }
12158
12159 #[test]
12160 fn font_chain_key_huge_duplicate_stack_does_not_hang() {
12161 let stack: Vec<FontSelector> = (0..5000).map(|_| sel("Arial")).collect();
12162 let k = FontChainKey::from_selectors(&stack);
12163 assert_eq!(k.font_families.len(), 1, "5000 dupes collapse to one entry");
12164 }
12165
12166 #[test]
12167 fn font_chain_key_is_a_stable_hash_map_key() {
12168 let a = FontChainKey::from_selectors(&[sel("Arial"), sel("Times")]);
12169 let b = FontChainKey::from_selectors(&[sel("Arial"), sel("Arial"), sel("Times")]);
12170 assert_eq!(a, b, "dedup makes the two stacks resolve to the same key");
12171 assert_eq!(hash_of(&a), hash_of(&b));
12172 }
12173
12174 #[test]
12175 fn font_chain_key_or_ref_from_stack_is_a_chain() {
12176 let fs = FontStack::Stack(vec![sel("Arial")]);
12177 let k = FontChainKeyOrRef::from_font_stack(&fs);
12178 assert!(!k.is_ref());
12179 assert_eq!(k.as_ref_ptr(), None);
12180 assert_eq!(
12181 k.as_chain().map(|c| c.font_families.clone()),
12182 Some(vec!["Arial".to_string()])
12183 );
12184 }
12185
12186 #[test]
12187 fn font_chain_key_or_ref_ref_variant_accessors_at_boundaries() {
12188 for ptr in [0_usize, 1, usize::MAX] {
12189 let k = FontChainKeyOrRef::Ref(ptr);
12190 assert!(k.is_ref());
12191 assert_eq!(k.as_ref_ptr(), Some(ptr));
12192 assert!(k.as_chain().is_none());
12193 }
12194 assert_ne!(
12196 FontChainKeyOrRef::Ref(0),
12197 FontChainKeyOrRef::Chain(FontChainKey::from_selectors(&[]))
12198 );
12199 }
12200
12201 #[test]
12202 fn font_stack_default_is_a_single_serif_selector() {
12203 let fs = FontStack::default();
12204 assert!(!fs.is_ref());
12205 assert!(fs.as_ref().is_none());
12206 assert_eq!(fs.as_stack().map(<[FontSelector]>::len), Some(1));
12207 assert_eq!(fs.first_selector().map(|s| s.family.as_str()), Some("serif"));
12208 assert_eq!(fs.first_family(), "serif");
12209 }
12210
12211 #[test]
12212 fn font_stack_empty_stack_reports_serif_placeholder_but_no_first_selector() {
12213 let fs = FontStack::Stack(Vec::new());
12214 assert_eq!(fs.as_stack().map(<[FontSelector]>::len), Some(0));
12215 assert!(fs.first_selector().is_none());
12216 assert_eq!(
12217 fs.first_family(),
12218 "serif",
12219 "an EMPTY stack must not panic; it reports the serif fallback"
12220 );
12221 }
12222
12223 #[test]
12224 fn font_hash_invalid_is_zero_and_is_the_default() {
12225 assert_eq!(FontHash::invalid().font_hash, 0);
12226 assert_eq!(FontHash::default(), FontHash::invalid());
12227 assert_eq!(FontHash::from_hash(0), FontHash::invalid());
12228 assert_eq!(FontHash::from_hash(u64::MAX).font_hash, u64::MAX);
12229 assert_ne!(FontHash::from_hash(u64::MAX), FontHash::invalid());
12230 }
12231
12232 #[test]
12237 fn layout_font_metrics_baseline_scaled_typical_and_zero_font_size() {
12238 let fm = std_metrics();
12239 approx(fm.baseline_scaled(16.0), 12.8); assert_eq!(fm.baseline_scaled(0.0), 0.0);
12241 approx(fm.baseline_scaled(-16.0), -12.8);
12242 }
12243
12244 #[test]
12245 fn layout_font_metrics_zero_upem_divides_by_zero_instead_of_guarding() {
12246 let fm = metrics(0, 800.0, -200.0, 0.0);
12250 assert!(fm.baseline_scaled(16.0).is_infinite());
12251 assert!(fm.cap_height_scaled(16.0).is_infinite());
12252
12253 let zero = metrics(0, 0.0, 0.0, 0.0);
12255 assert!(zero.baseline_scaled(16.0).is_nan());
12256 }
12257
12258 #[test]
12259 fn layout_font_metrics_x_height_falls_back_to_half_em() {
12260 let fm = std_metrics(); assert_eq!(fm.x_height_scaled(16.0), 8.0, "fallback is 0.5em");
12262 assert_eq!(fm.x_height_scaled(0.0), 0.0);
12263
12264 let mut with_xh = std_metrics();
12265 with_xh.x_height = Some(500.0);
12266 approx(with_xh.x_height_scaled(16.0), 8.0);
12267 with_xh.x_height = Some(0.0);
12268 assert_eq!(
12269 with_xh.x_height_scaled(16.0),
12270 0.0,
12271 "an explicit sxHeight of 0 must NOT re-trigger the 0.5em fallback"
12272 );
12273 }
12274
12275 #[test]
12276 fn layout_font_metrics_cap_height_falls_back_to_ascent() {
12277 let fm = std_metrics(); assert_eq!(fm.cap_height_scaled(16.0), fm.baseline_scaled(16.0));
12279
12280 let mut with_cap = std_metrics();
12281 with_cap.cap_height = Some(700.0);
12282 approx(with_cap.cap_height_scaled(16.0), 11.2);
12283 }
12284
12285 #[test]
12286 fn layout_font_metrics_nan_font_size_propagates_without_panicking() {
12287 let fm = std_metrics();
12288 assert!(fm.baseline_scaled(f32::NAN).is_nan());
12289 assert!(fm.x_height_scaled(f32::NAN).is_nan());
12290 assert!(fm.cap_height_scaled(f32::NAN).is_nan());
12291 assert!(fm.baseline_scaled(f32::INFINITY).is_infinite());
12292 }
12293
12294 #[test]
12295 fn layout_font_metrics_synthesized_baselines_span_exactly_one_em() {
12296 let fm = std_metrics();
12297 assert_eq!(fm.central_baseline(), 300.0); assert_eq!(fm.em_over(), 800.0); assert_eq!(fm.em_under(), -200.0); assert_eq!(
12301 fm.em_over() - fm.em_under(),
12302 f32::from(fm.units_per_em),
12303 "em-over minus em-under is by definition 1em"
12304 );
12305 }
12306
12307 #[test]
12308 fn layout_font_metrics_baselines_at_u16_max_upem_and_zero_upem() {
12309 let big = metrics(u16::MAX, 0.0, 0.0, 0.0);
12310 assert_eq!(big.central_baseline(), 0.0);
12311 assert_eq!(big.em_over(), f32::from(u16::MAX) / 2.0);
12312 assert_eq!(big.em_under(), -f32::from(u16::MAX) / 2.0);
12313
12314 let zero = metrics(0, 100.0, -50.0, 0.0);
12315 assert_eq!(zero.em_over(), zero.central_baseline());
12316 assert_eq!(zero.em_under(), zero.central_baseline());
12317 }
12318
12319 #[test]
12320 fn layout_font_metrics_central_baseline_with_infinite_extents_is_nan() {
12321 let fm = metrics(1000, f32::INFINITY, f32::NEG_INFINITY, 0.0);
12322 assert!(fm.central_baseline().is_nan(), "midpoint(inf, -inf) is NaN");
12323 assert!(fm.em_over().is_nan());
12324 }
12325
12326 #[test]
12331 fn round_eq_rounds_half_away_from_zero() {
12332 assert!(round_eq(0.4, -0.4), "both round to 0");
12333 assert!(round_eq(1.5, 2.4), "1.5 rounds away from zero to 2");
12334 assert!(!round_eq(1.4, 1.5));
12335 assert!(round_eq(-1.5, -2.0));
12336 }
12337
12338 #[test]
12339 fn round_eq_treats_nan_as_equal_to_everything_rounding_to_zero() {
12340 assert!(round_eq(f32::NAN, f32::NAN));
12345 assert!(round_eq(f32::NAN, 0.0));
12346 assert!(round_eq(f32::NAN, 0.49));
12347 assert!(!round_eq(f32::NAN, 1.0));
12348
12349 assert_eq!(
12350 Rect {
12351 x: f32::NAN,
12352 y: 0.0,
12353 width: 0.0,
12354 height: 0.0
12355 },
12356 Rect::default(),
12357 "a NaN-x Rect compares equal to the zero Rect"
12358 );
12359 }
12360
12361 #[test]
12362 fn round_eq_saturates_infinity_and_f32_max_to_the_same_isize() {
12363 assert!(round_eq(f32::INFINITY, f32::MAX));
12365 assert!(round_eq(f32::NEG_INFINITY, f32::MIN));
12366 assert!(!round_eq(f32::INFINITY, f32::NEG_INFINITY));
12367
12368 assert_eq!(
12369 Size::new(f32::INFINITY, 0.0),
12370 Size::new(f32::MAX, 0.0),
12371 "saturating cast collapses inf and f32::MAX into one bucket"
12372 );
12373 }
12374
12375 #[test]
12380 fn size_zero_is_the_neutral_element_and_new_preserves_bits() {
12381 assert_eq!(Size::zero(), Size::new(0.0, 0.0));
12382 assert_eq!(Size::zero().width, 0.0);
12383 assert_eq!(Size::zero(), Size::default());
12384
12385 let weird = Size::new(f32::NAN, f32::INFINITY);
12386 assert!(weird.width.is_nan(), "the constructor must not sanitize");
12387 assert!(weird.height.is_infinite());
12388 }
12389
12390 #[test]
12391 fn bounding_box_of_empty_and_single_point_is_zero() {
12392 assert_eq!(calculate_bounding_box_size(&[]), Size::zero());
12393 assert_eq!(
12394 calculate_bounding_box_size(&[Point { x: 5.0, y: -5.0 }]),
12395 Size::zero()
12396 );
12397 }
12398
12399 #[test]
12400 fn bounding_box_spans_negative_coordinates() {
12401 let pts = [
12402 Point { x: -10.0, y: -20.0 },
12403 Point { x: 30.0, y: 5.0 },
12404 Point { x: 0.0, y: 0.0 },
12405 ];
12406 assert_eq!(calculate_bounding_box_size(&pts), Size::new(40.0, 25.0));
12407 }
12408
12409 #[test]
12410 fn bounding_box_of_all_nan_points_collapses_to_zero() {
12411 let pts = [Point {
12413 x: f32::NAN,
12414 y: f32::NAN,
12415 }];
12416 assert_eq!(calculate_bounding_box_size(&pts), Size::zero());
12417 }
12418
12419 #[test]
12420 fn bounding_box_of_extreme_points_overflows_to_infinity_without_panicking() {
12421 let pts = [
12422 Point {
12423 x: f32::MIN,
12424 y: f32::MIN,
12425 },
12426 Point {
12427 x: f32::MAX,
12428 y: f32::MAX,
12429 },
12430 ];
12431 let s = calculate_bounding_box_size(&pts);
12432 assert!(s.width.is_infinite() && s.height.is_infinite());
12433 }
12434
12435 #[test]
12436 fn shape_definition_get_size_for_each_variant() {
12437 assert_eq!(
12438 ShapeDefinition::Rectangle {
12439 size: Size::new(3.0, 4.0),
12440 corner_radius: None
12441 }
12442 .get_size(),
12443 Size::new(3.0, 4.0)
12444 );
12445 assert_eq!(
12446 ShapeDefinition::Circle { radius: 10.0 }.get_size(),
12447 Size::new(20.0, 20.0)
12448 );
12449 assert_eq!(
12450 ShapeDefinition::Ellipse {
12451 radii: Size::new(5.0, 2.0)
12452 }
12453 .get_size(),
12454 Size::new(10.0, 4.0)
12455 );
12456 assert_eq!(
12457 ShapeDefinition::Polygon { points: Vec::new() }.get_size(),
12458 Size::zero()
12459 );
12460 assert_eq!(
12461 ShapeDefinition::Path {
12462 segments: Vec::new()
12463 }
12464 .get_size(),
12465 Size::zero()
12466 );
12467 }
12468
12469 #[test]
12470 fn shape_definition_negative_circle_radius_yields_a_negative_size() {
12471 let s = ShapeDefinition::Circle { radius: -10.0 }.get_size();
12474 assert_eq!(s.width, -20.0);
12475 assert_eq!(s.height, -20.0);
12476 }
12477
12478 #[test]
12479 fn shape_definition_path_of_only_close_segments_is_zero_sized() {
12480 let s = ShapeDefinition::Path {
12481 segments: vec![PathSegment::Close, PathSegment::Close],
12482 }
12483 .get_size();
12484 assert_eq!(s, Size::zero(), "Close contributes no points");
12485 }
12486
12487 #[test]
12488 fn shape_definition_path_bounding_box_includes_control_points() {
12489 let s = ShapeDefinition::Path {
12490 segments: vec![
12491 PathSegment::MoveTo(Point { x: 0.0, y: 0.0 }),
12492 PathSegment::QuadTo {
12493 control: Point { x: 50.0, y: 100.0 },
12494 end: Point { x: 100.0, y: 0.0 },
12495 },
12496 ],
12497 }
12498 .get_size();
12499 assert_eq!(
12500 s,
12501 Size::new(100.0, 100.0),
12502 "the control point (not the true curve extremum) sets the height"
12503 );
12504 }
12505
12506 #[test]
12511 fn shape_boundary_inflate_by_zero_is_identity() {
12512 let r = ShapeBoundary::Rectangle(Rect {
12513 x: 1.0,
12514 y: 2.0,
12515 width: 3.0,
12516 height: 4.0,
12517 });
12518 assert_eq!(r.inflate(0.0), r);
12519 let c = ShapeBoundary::Circle {
12520 center: Point { x: 0.0, y: 0.0 },
12521 radius: 5.0,
12522 };
12523 assert_eq!(c.inflate(0.0), c);
12524 }
12525
12526 #[test]
12527 fn shape_boundary_inflate_rectangle_clamps_negative_dimensions_to_zero() {
12528 let r = ShapeBoundary::Rectangle(Rect {
12529 x: 0.0,
12530 y: 0.0,
12531 width: 10.0,
12532 height: 10.0,
12533 });
12534 match r.inflate(-100.0) {
12535 ShapeBoundary::Rectangle(out) => {
12536 assert_eq!(out.width, 0.0, "over-deflation must clamp, not go negative");
12537 assert_eq!(out.height, 0.0);
12538 assert_eq!(out.x, 100.0, "the origin is NOT clamped");
12539 }
12540 other => panic!("expected Rectangle, got {other:?}"),
12541 }
12542 }
12543
12544 #[test]
12545 fn shape_boundary_inflate_nan_margin_zeroes_the_rectangle_extent() {
12546 let r = ShapeBoundary::Rectangle(Rect {
12550 x: 0.0,
12551 y: 0.0,
12552 width: 10.0,
12553 height: 10.0,
12554 });
12555 match r.inflate(f32::NAN) {
12556 ShapeBoundary::Rectangle(out) => {
12557 assert_eq!(out.width, 0.0);
12558 assert_eq!(out.height, 0.0);
12559 assert!(out.x.is_nan());
12560 }
12561 other => panic!("expected Rectangle, got {other:?}"),
12562 }
12563 }
12564
12565 #[test]
12566 fn shape_boundary_inflate_circle_radius_is_unclamped() {
12567 let c = ShapeBoundary::Circle {
12568 center: Point { x: 1.0, y: 2.0 },
12569 radius: 5.0,
12570 };
12571 match c.inflate(-50.0) {
12572 ShapeBoundary::Circle { center, radius } => {
12573 assert_eq!(center, Point { x: 1.0, y: 2.0 });
12574 assert_eq!(radius, -45.0, "circle radius is NOT clamped at 0 (unlike Rect)");
12575 }
12576 other => panic!("expected Circle, got {other:?}"),
12577 }
12578 match c.inflate(f32::INFINITY) {
12579 ShapeBoundary::Circle { radius, .. } => assert!(radius.is_infinite()),
12580 other => panic!("expected Circle, got {other:?}"),
12581 }
12582 }
12583
12584 #[test]
12585 fn shape_boundary_inflate_is_a_documented_no_op_for_polygon_and_path() {
12586 let p = ShapeBoundary::Polygon {
12587 points: vec![Point { x: 0.0, y: 0.0 }, Point { x: 1.0, y: 1.0 }],
12588 };
12589 assert_eq!(p.inflate(10.0), p, "polygon inflation is not implemented");
12590 let path = ShapeBoundary::Path {
12591 segments: vec![PathSegment::MoveTo(Point { x: 0.0, y: 0.0 })],
12592 };
12593 assert_eq!(path.inflate(10.0), path, "path inflation is not implemented");
12594 }
12595
12596 #[test]
12601 fn resolve_effective_alignment_passes_through_for_non_last_lines() {
12602 for ta in [
12603 TextAlign::Left,
12604 TextAlign::Right,
12605 TextAlign::Center,
12606 TextAlign::Justify,
12607 TextAlign::Start,
12608 TextAlign::End,
12609 TextAlign::JustifyAll,
12610 ] {
12611 assert_eq!(
12612 resolve_effective_alignment(ta, TextAlign::Right, false),
12613 ta,
12614 "text-align-last must not touch a non-last line"
12615 );
12616 }
12617 }
12618
12619 #[test]
12620 fn resolve_effective_alignment_last_line_justify_degrades_to_start() {
12621 assert_eq!(
12622 resolve_effective_alignment(TextAlign::Justify, TextAlign::default(), true),
12623 TextAlign::Start
12624 );
12625 assert_eq!(
12626 resolve_effective_alignment(TextAlign::Center, TextAlign::default(), true),
12627 TextAlign::Center,
12628 "non-justify alignments survive onto the last line"
12629 );
12630 }
12631
12632 #[test]
12633 fn resolve_effective_alignment_explicit_text_align_last_left_is_indistinguishable_from_auto() {
12634 assert_eq!(
12638 resolve_effective_alignment(TextAlign::Center, TextAlign::Left, true),
12639 TextAlign::Center
12640 );
12641 assert_eq!(
12643 resolve_effective_alignment(TextAlign::Center, TextAlign::Right, true),
12644 TextAlign::Right
12645 );
12646 assert_eq!(
12647 resolve_effective_alignment(TextAlign::Justify, TextAlign::Justify, true),
12648 TextAlign::Justify
12649 );
12650 }
12651
12652 #[test]
12657 fn spacing_resolve_px_default_is_zero_and_font_size_independent() {
12658 assert_eq!(Spacing::default(), Spacing::Px(0));
12659 assert_eq!(Spacing::default().resolve_px(16.0), 0.0);
12660 assert_eq!(Spacing::Px(0).resolve_px(f32::NAN), 0.0);
12661 }
12662
12663 #[test]
12664 fn spacing_resolve_px_at_i32_extremes_stays_finite() {
12665 let hi = Spacing::Px(i32::MAX).resolve_px(16.0);
12666 let lo = Spacing::Px(i32::MIN).resolve_px(16.0);
12667 assert!(hi.is_finite() && hi > 2.0e9, "got {hi}");
12668 assert!(lo.is_finite() && lo < -2.0e9, "got {lo}");
12669 assert_eq!(lo, -2147483648.0);
12670 }
12671
12672 #[test]
12673 fn spacing_resolve_px_em_scales_with_font_size() {
12674 assert_eq!(Spacing::Em(2.0).resolve_px(16.0), 32.0);
12675 assert_eq!(Spacing::Em(2.0).resolve_px(0.0), 0.0);
12676 assert_eq!(Spacing::Em(-0.5).resolve_px(16.0), -8.0);
12677 assert_eq!(Spacing::PxF(0.4).resolve_px(999.0), 0.4, "PxF ignores font size");
12678 }
12679
12680 #[test]
12681 fn spacing_resolve_px_nan_and_overflow_are_defined() {
12682 assert!(Spacing::Em(f32::NAN).resolve_px(16.0).is_nan());
12683 assert!(Spacing::Em(1.0).resolve_px(f32::NAN).is_nan());
12684 assert!(Spacing::PxF(f32::NAN).resolve_px(16.0).is_nan());
12685 assert!(Spacing::Em(f32::MAX).resolve_px(2.0).is_infinite());
12686 assert!(Spacing::Em(0.0).resolve_px(f32::INFINITY).is_nan());
12688 }
12689
12690 #[test]
12691 fn spacing_px_and_pxf_of_the_same_value_are_distinct_cache_keys() {
12692 assert_ne!(Spacing::Px(1), Spacing::PxF(1.0));
12693 assert_ne!(hash_of(&Spacing::Px(1)), hash_of(&Spacing::PxF(1.0)));
12694 assert_eq!(Spacing::Px(1).resolve_px(16.0), Spacing::PxF(1.0).resolve_px(16.0));
12695 }
12696
12697 #[test]
12702 fn bidi_direction_is_rtl() {
12703 assert!(!BidiDirection::Ltr.is_rtl());
12704 assert!(BidiDirection::Rtl.is_rtl());
12705 }
12706
12707 #[test]
12708 fn bidi_level_parity_defines_rtl_across_the_whole_u8_range() {
12709 for lvl in [0_u8, 1, 2, 3, 126, 127, 254, u8::MAX] {
12710 let b = BidiLevel::new(lvl);
12711 assert_eq!(b.level(), lvl, "level() must round-trip new()");
12712 assert_eq!(b.is_rtl(), lvl % 2 == 1, "odd embedding levels are RTL");
12713 }
12714 }
12715
12716 #[test]
12717 fn writing_mode_is_advance_horizontal_for_every_variant() {
12718 assert!(WritingMode::HorizontalTb.is_advance_horizontal());
12719 assert!(WritingMode::SidewaysRl.is_advance_horizontal());
12720 assert!(WritingMode::SidewaysLr.is_advance_horizontal());
12721 assert!(!WritingMode::VerticalRl.is_advance_horizontal());
12722 assert!(!WritingMode::VerticalLr.is_advance_horizontal());
12723 assert_eq!(WritingMode::default(), WritingMode::HorizontalTb);
12724 }
12725
12726 #[test]
12727 fn writing_mode_get_direction_only_horizontal_defers_to_content() {
12728 assert_eq!(WritingMode::HorizontalTb.get_direction(), None);
12729 assert_eq!(WritingMode::VerticalRl.get_direction(), Some(BidiDirection::Rtl));
12730 assert_eq!(WritingMode::VerticalLr.get_direction(), Some(BidiDirection::Ltr));
12731 assert_eq!(WritingMode::SidewaysRl.get_direction(), Some(BidiDirection::Rtl));
12732 assert_eq!(WritingMode::SidewaysLr.get_direction(), Some(BidiDirection::Ltr));
12733 }
12734
12735 #[test]
12740 fn unified_constraints_default_is_horizontal_max_content() {
12741 let c = UnifiedConstraints::default();
12742 assert!(!c.is_vertical());
12743 assert_eq!(c.available_width, AvailableSpace::MaxContent);
12744 assert_eq!(c.columns, 1);
12745 assert_eq!(c, UnifiedConstraints::default());
12746 assert_eq!(
12747 hash_of(&c),
12748 hash_of(&UnifiedConstraints::default()),
12749 "Hash/Eq must agree for the default constraints"
12750 );
12751 }
12752
12753 #[test]
12754 fn unified_constraints_is_vertical_only_for_the_two_vertical_modes() {
12755 let mut c = UnifiedConstraints::default();
12756 for (wm, want) in [
12757 (WritingMode::HorizontalTb, false),
12758 (WritingMode::VerticalRl, true),
12759 (WritingMode::VerticalLr, true),
12760 (WritingMode::SidewaysRl, false),
12761 (WritingMode::SidewaysLr, false),
12762 ] {
12763 c.writing_mode = Some(wm);
12764 assert_eq!(c.is_vertical(), want, "{wm:?}");
12765 }
12766 c.writing_mode = None;
12767 assert!(!c.is_vertical());
12768 }
12769
12770 #[test]
12771 fn unified_constraints_direction_uses_fallback_unless_the_writing_mode_forces_one() {
12772 let mut c = UnifiedConstraints::default();
12773 assert_eq!(c.direction(BidiDirection::Rtl), BidiDirection::Rtl);
12775 c.writing_mode = Some(WritingMode::HorizontalTb);
12777 assert_eq!(c.direction(BidiDirection::Rtl), BidiDirection::Rtl);
12778 c.writing_mode = Some(WritingMode::VerticalRl);
12780 assert_eq!(c.direction(BidiDirection::Ltr), BidiDirection::Rtl);
12781 }
12782
12783 #[test]
12784 fn unified_constraints_resolved_line_height_uses_the_strut_for_normal() {
12785 let mut c = UnifiedConstraints::default();
12786 assert_eq!(
12787 c.resolved_line_height(),
12788 DEFAULT_STRUT_ASCENT + DEFAULT_STRUT_DESCENT
12789 );
12790 assert_eq!(c.resolved_line_height(), 16.0);
12791
12792 c.line_height = LineHeight::Px(0.0);
12793 assert_eq!(c.resolved_line_height(), 0.0, "an explicit 0 is honoured");
12794
12795 c.line_height = LineHeight::Px(-5.0);
12797 assert_eq!(c.resolved_line_height(), -5.0);
12798 c.line_height = LineHeight::Px(f32::NAN);
12799 assert!(c.resolved_line_height().is_nan());
12800 }
12801
12802 #[test]
12803 fn unified_constraints_partial_eq_is_rounding_tolerant() {
12804 let mut a = UnifiedConstraints::default();
12806 let mut b = UnifiedConstraints::default();
12807 a.strut_ascent = 12.8;
12808 b.strut_ascent = 12.9;
12809 assert_eq!(a, b, "12.8 and 12.9 both round to 13");
12810 b.strut_ascent = 14.0;
12811 assert_ne!(a, b);
12812 }
12813
12814 #[test]
12819 fn text_decoration_from_css_maps_each_variant_exclusively() {
12820 use azul_css::props::style::text::StyleTextDecoration;
12821 let none = TextDecoration::from_css(StyleTextDecoration::None);
12822 assert_eq!(none, TextDecoration::default());
12823 assert!(!none.underline && !none.strikethrough && !none.overline);
12824
12825 let u = TextDecoration::from_css(StyleTextDecoration::Underline);
12826 assert!(u.underline && !u.strikethrough && !u.overline);
12827
12828 let o = TextDecoration::from_css(StyleTextDecoration::Overline);
12829 assert!(!o.underline && !o.strikethrough && o.overline);
12830
12831 let lt = TextDecoration::from_css(StyleTextDecoration::LineThrough);
12832 assert!(!lt.underline && lt.strikethrough && !lt.overline);
12833 }
12834
12835 #[test]
12840 fn inline_border_info_default_has_no_border_and_no_chrome() {
12841 let b = InlineBorderInfo::default();
12842 assert!(!b.has_border());
12843 assert!(!b.has_chrome());
12844 assert_eq!(b.left_inset(), 0.0);
12845 assert_eq!(b.right_inset(), 0.0);
12846 assert_eq!(b.top_inset(), 0.0);
12847 assert_eq!(b.bottom_inset(), 0.0);
12848 }
12849
12850 #[test]
12851 fn inline_border_info_negative_widths_do_not_count_as_a_border() {
12852 let b = InlineBorderInfo {
12853 top: -1.0,
12854 right: -1.0,
12855 bottom: -1.0,
12856 left: -1.0,
12857 ..InlineBorderInfo::default()
12858 };
12859 assert!(!b.has_border(), "the predicate is strictly `> 0.0`");
12860 assert!(!b.has_chrome());
12861 assert_eq!(b.left_inset(), -1.0);
12863 }
12864
12865 #[test]
12866 fn inline_border_info_padding_alone_is_chrome_but_not_a_border() {
12867 let b = InlineBorderInfo {
12868 padding_left: 4.0,
12869 ..InlineBorderInfo::default()
12870 };
12871 assert!(!b.has_border());
12872 assert!(b.has_chrome());
12873 assert_eq!(b.left_inset(), 4.0);
12874 }
12875
12876 #[test]
12877 fn inline_border_info_nan_border_width_is_not_a_border() {
12878 let b = InlineBorderInfo {
12879 top: f32::NAN,
12880 ..InlineBorderInfo::default()
12881 };
12882 assert!(!b.has_border(), "NaN > 0.0 is false");
12883 assert!(b.top_inset().is_nan(), "but the inset still carries the NaN");
12884 }
12885
12886 #[test]
12887 fn inline_border_info_split_insets_swap_edges_in_rtl() {
12888 let base = InlineBorderInfo {
12889 left: 2.0,
12890 right: 3.0,
12891 padding_left: 1.0,
12892 padding_right: 1.0,
12893 ..InlineBorderInfo::default()
12894 };
12895
12896 let ltr_first = InlineBorderInfo {
12898 is_first_fragment: true,
12899 is_last_fragment: false,
12900 ..base
12901 };
12902 assert_eq!(ltr_first.left_inset(), 3.0);
12903 assert_eq!(ltr_first.right_inset(), 0.0);
12904
12905 let ltr_last = InlineBorderInfo {
12906 is_first_fragment: false,
12907 is_last_fragment: true,
12908 ..base
12909 };
12910 assert_eq!(ltr_last.left_inset(), 0.0);
12911 assert_eq!(ltr_last.right_inset(), 4.0);
12912
12913 let rtl_first = InlineBorderInfo {
12915 is_first_fragment: true,
12916 is_last_fragment: false,
12917 is_rtl: true,
12918 ..base
12919 };
12920 assert_eq!(rtl_first.left_inset(), 0.0);
12921 assert_eq!(rtl_first.right_inset(), 4.0);
12922
12923 let rtl_last = InlineBorderInfo {
12924 is_first_fragment: false,
12925 is_last_fragment: true,
12926 is_rtl: true,
12927 ..base
12928 };
12929 assert_eq!(rtl_last.left_inset(), 3.0);
12930 assert_eq!(rtl_last.right_inset(), 0.0);
12931
12932 let middle = InlineBorderInfo {
12934 is_first_fragment: false,
12935 is_last_fragment: false,
12936 ..base
12937 };
12938 assert_eq!(middle.left_inset(), 0.0);
12939 assert_eq!(middle.right_inset(), 0.0);
12940 let tall = InlineBorderInfo {
12942 top: 1.0,
12943 bottom: 2.0,
12944 padding_top: 3.0,
12945 padding_bottom: 4.0,
12946 is_first_fragment: false,
12947 is_last_fragment: false,
12948 ..InlineBorderInfo::default()
12949 };
12950 assert_eq!(tall.top_inset(), 4.0);
12951 assert_eq!(tall.bottom_inset(), 6.0);
12952 }
12953
12954 #[test]
12959 fn style_layout_eq_ignores_render_only_properties() {
12960 let a = StyleProperties::default();
12961 let b = StyleProperties {
12962 color: ColorU {
12963 r: 255,
12964 g: 0,
12965 b: 0,
12966 a: 255,
12967 },
12968 background_color: Some(ColorU::TRANSPARENT),
12969 text_decoration: TextDecoration {
12970 underline: true,
12971 strikethrough: false,
12972 overline: false,
12973 },
12974 border: Some(InlineBorderInfo::default()),
12975 ..StyleProperties::default()
12976 };
12977 assert_ne!(a, b, "the full PartialEq DOES see the colour change");
12978 assert!(
12979 a.layout_eq(&b),
12980 "but layout_eq must ignore colour/decoration/border"
12981 );
12982 assert_eq!(a.layout_hash(), b.layout_hash());
12983 }
12984
12985 #[test]
12986 fn style_layout_eq_sees_sub_pixel_font_size_changes() {
12987 let a = StyleProperties::default();
12988 let b = StyleProperties {
12989 font_size_px: 16.4,
12990 ..StyleProperties::default()
12991 };
12992 assert!(
12993 !a.layout_eq(&b),
12994 "16.0 vs 16.4 must NOT share a shaping-cache entry"
12995 );
12996 }
12997
12998 #[test]
12999 fn style_layout_eq_sees_spacing_and_font_stack_changes() {
13000 let base = StyleProperties::default();
13001
13002 let spaced = StyleProperties {
13003 letter_spacing: Spacing::PxF(0.5),
13004 ..StyleProperties::default()
13005 };
13006 assert!(!base.layout_eq(&spaced));
13007
13008 let worded = StyleProperties {
13009 word_spacing: Spacing::Em(0.1),
13010 ..StyleProperties::default()
13011 };
13012 assert!(!base.layout_eq(&worded));
13013
13014 let other_font = StyleProperties {
13015 font_stack: FontStack::Stack(vec![sel("Arial")]),
13016 ..StyleProperties::default()
13017 };
13018 assert!(!base.layout_eq(&other_font));
13019
13020 let vertical = StyleProperties {
13021 writing_mode: WritingMode::VerticalRl,
13022 ..StyleProperties::default()
13023 };
13024 assert!(!base.layout_eq(&vertical));
13025 }
13026
13027 #[test]
13028 fn style_layout_hash_is_stable_across_repeated_calls() {
13029 let s = StyleProperties::default();
13030 assert_eq!(s.layout_hash(), s.layout_hash());
13031 assert!(s.layout_eq(&StyleProperties::default()));
13032 }
13033
13034 #[test]
13035 fn style_layout_eq_treats_two_nan_font_sizes_as_equal() {
13036 let a = StyleProperties {
13038 font_size_px: f32::NAN,
13039 ..StyleProperties::default()
13040 };
13041 let b = StyleProperties {
13042 font_size_px: f32::NAN,
13043 ..StyleProperties::default()
13044 };
13045 assert!(a.layout_eq(&b));
13046 assert!(!a.layout_eq(&StyleProperties::default()));
13047 }
13048
13049 #[test]
13050 fn style_apply_override_with_an_empty_partial_changes_nothing() {
13051 let base = StyleProperties::default();
13052 let out = base.apply_override(&PartialStyleProperties::default());
13053 assert_eq!(out, base);
13054 }
13055
13056 #[test]
13057 fn style_apply_override_applies_only_the_some_fields() {
13058 let base = StyleProperties::default();
13059 let partial = PartialStyleProperties {
13060 font_size_px: Some(32.0),
13061 letter_spacing: Some(Spacing::PxF(1.5)),
13062 ..PartialStyleProperties::default()
13063 };
13064 let out = base.apply_override(&partial);
13065 assert_eq!(out.font_size_px, 32.0);
13066 assert_eq!(out.letter_spacing, Spacing::PxF(1.5));
13067 assert_eq!(out.word_spacing, base.word_spacing);
13069 assert_eq!(out.tab_size, base.tab_size);
13070 assert_eq!(out.font_stack, base.font_stack);
13071 assert!(!out.layout_eq(&base));
13072 }
13073
13074 #[test]
13075 fn style_apply_override_can_inject_nan_font_size() {
13076 let base = StyleProperties::default();
13077 let partial = PartialStyleProperties {
13078 font_size_px: Some(f32::NAN),
13079 ..PartialStyleProperties::default()
13080 };
13081 let out = base.apply_override(&partial);
13082 assert!(out.font_size_px.is_nan(), "no validation happens here");
13083 }
13084
13085 #[test]
13090 fn classify_character_covers_each_class() {
13091 assert_eq!(classify_character(0x0020), CharacterClass::Space);
13092 assert_eq!(classify_character(0x00A0), CharacterClass::Space);
13093 assert_eq!(classify_character(0x3000), CharacterClass::Space);
13094 assert_eq!(classify_character('.' as u32), CharacterClass::Punctuation);
13095 assert_eq!(classify_character('~' as u32), CharacterClass::Punctuation);
13096 assert_eq!(classify_character('a' as u32), CharacterClass::Letter);
13097 assert_eq!(classify_character(0x4E00), CharacterClass::Ideograph);
13098 assert_eq!(classify_character(0x9FFF), CharacterClass::Ideograph);
13099 assert_eq!(classify_character(0x0301), CharacterClass::Combining);
13100 }
13101
13102 #[test]
13103 fn classify_character_at_u32_extremes_defaults_to_letter() {
13104 assert_eq!(classify_character(0), CharacterClass::Letter);
13105 assert_eq!(classify_character(u32::MAX), CharacterClass::Letter);
13106 assert_eq!(classify_character(0x4DFF), CharacterClass::Letter);
13108 assert_eq!(classify_character(0xA000), CharacterClass::Letter);
13109 }
13110
13111 #[test]
13112 fn get_justification_priority_is_strictly_ordered_space_to_combining() {
13113 let p = |c| get_justification_priority(c);
13114 assert_eq!(p(CharacterClass::Space), 0);
13115 assert_eq!(p(CharacterClass::Combining), 255);
13116 assert!(p(CharacterClass::Space) < p(CharacterClass::Punctuation));
13117 assert!(p(CharacterClass::Punctuation) < p(CharacterClass::Ideograph));
13118 assert!(p(CharacterClass::Ideograph) < p(CharacterClass::Letter));
13119 assert!(p(CharacterClass::Letter) < p(CharacterClass::Symbol));
13120 assert!(p(CharacterClass::Symbol) < p(CharacterClass::Combining));
13121 }
13122
13123 #[test]
13128 fn is_hanging_punctuation_char_only_stops_and_commas() {
13129 assert!(is_hanging_punctuation_char(','));
13130 assert!(is_hanging_punctuation_char('.'));
13131 assert!(is_hanging_punctuation_char('\u{3001}'));
13132 assert!(is_hanging_punctuation_char('\u{FF0E}'));
13133 assert!(!is_hanging_punctuation_char(';'));
13134 assert!(!is_hanging_punctuation_char(' '));
13135 assert!(!is_hanging_punctuation_char('\0'));
13136 assert!(!is_hanging_punctuation_char(char::MAX));
13137 }
13138
13139 #[test]
13140 fn is_word_char_is_alphanumeric_or_underscore() {
13141 assert!(is_word_char('a'));
13142 assert!(is_word_char('Z'));
13143 assert!(is_word_char('9'));
13144 assert!(is_word_char('_'));
13145 assert!(is_word_char('é'), "non-ASCII letters are word chars");
13146 assert!(is_word_char('中'), "ideographs are alphanumeric");
13147 assert!(!is_word_char(' '));
13148 assert!(!is_word_char('-'));
13149 assert!(!is_word_char('.'));
13150 assert!(!is_word_char('\u{00A0}'));
13151 assert!(!is_word_char('\0'));
13152 }
13153
13154 #[test]
13155 fn is_word_separator_char_excludes_tabs_and_fixed_width_spaces() {
13156 assert!(is_word_separator_char(' '));
13157 assert!(is_word_separator_char('\u{00A0}'), "NBSP IS a word separator");
13158 assert!(is_word_separator_char('\u{1680}'));
13159 assert!(is_word_separator_char('\u{202F}'));
13160 assert!(is_word_separator_char('\u{10100}'));
13161
13162 assert!(!is_word_separator_char('\u{2000}'));
13164 assert!(!is_word_separator_char('\u{200A}'));
13165 assert!(!is_word_separator_char('\u{3000}'), "ideographic space excluded");
13166 assert!(!is_word_separator_char('\t'));
13168 assert!(!is_word_separator_char('\n'));
13169 assert!(!is_word_separator_char('.'));
13170 assert!(!is_word_separator_char(char::MAX));
13171 }
13172
13173 #[test]
13174 fn is_cursive_script_char_boundaries() {
13175 assert!(!is_cursive_script_char('\u{05FF}'), "one below Arabic");
13176 assert!(is_cursive_script_char('\u{0600}'), "Arabic block start");
13177 assert!(is_cursive_script_char('\u{06FF}'), "Arabic block end");
13178 assert!(is_cursive_script_char('\u{0700}'), "Syriac");
13179 assert!(is_cursive_script_char('\u{1800}'), "Mongolian");
13180 assert!(is_cursive_script_char('\u{10D00}'), "Hanifi Rohingya (astral)");
13181 assert!(!is_cursive_script_char('a'));
13182 assert!(!is_cursive_script_char('中'));
13183 assert!(!is_cursive_script_char('\0'));
13184 assert!(!is_cursive_script_char(char::MAX));
13185 }
13186
13187 #[test]
13188 fn is_cjk_character_boundaries() {
13189 assert!(is_cjk_character('中')); assert!(is_cjk_character('\u{4E00}'));
13191 assert!(is_cjk_character('\u{9FFF}'));
13192 assert!(is_cjk_character('\u{3040}'), "hiragana block");
13193 assert!(is_cjk_character('\u{30FF}'), "katakana block");
13194 assert!(is_cjk_character('\u{AC00}'), "hangul syllables");
13195 assert!(is_cjk_character('\u{FF01}'), "fullwidth forms");
13196 assert!(!is_cjk_character('\u{4DFF}'), "one below the ideograph block");
13197 assert!(!is_cjk_character('a'));
13198 assert!(!is_cjk_character('\0'));
13199 assert!(!is_cjk_character(char::MAX));
13200 }
13201
13202 #[test]
13203 fn break_control_predicates_are_disjoint() {
13204 assert!(is_break_suppressing_control('\u{200D}'));
13205 assert!(is_break_suppressing_control('\u{2060}'));
13206 assert!(is_break_suppressing_control('\u{FEFF}'));
13207 assert!(!is_break_suppressing_control(' '));
13208 assert!(!is_break_suppressing_control('\u{200B}'));
13209
13210 assert!(is_break_forcing_control('\u{200B}'));
13211 assert!(is_break_forcing_control('\u{2028}'));
13212 assert!(is_break_forcing_control('\u{2029}'));
13213 assert!(!is_break_forcing_control(' '));
13214 assert!(!is_break_forcing_control('\u{200D}'));
13215
13216 for ch in ['\u{200D}', '\u{2060}', '\u{FEFF}', '\u{200B}', '\u{2028}'] {
13217 assert!(
13218 !(is_break_suppressing_control(ch) && is_break_forcing_control(ch)),
13219 "{ch:?} cannot both force and suppress a break"
13220 );
13221 }
13222 }
13223
13224 #[test]
13225 fn is_small_kana_matches_only_the_cj_class() {
13226 assert!(is_small_kana('っ'));
13227 assert!(is_small_kana('ゃ'));
13228 assert!(is_small_kana('ッ'));
13229 assert!(is_small_kana('ー'), "prolonged sound mark is class CJ");
13230 assert!(!is_small_kana('つ'), "the FULL-size kana is not CJ");
13231 assert!(!is_small_kana('中'));
13232 assert!(!is_small_kana('a'));
13233 assert!(!is_small_kana('\0'));
13234 }
13235
13236 #[test]
13237 fn is_cjk_break_allowed_by_strictness_per_level() {
13238 use LineBreakStrictness::{Anywhere, Auto, Loose, Normal, Strict};
13239
13240 for ch in ['っ', '\u{301C}', '\u{2010}', '中'] {
13242 assert!(is_cjk_break_allowed_by_strictness(ch, None, Anywhere), "{ch:?}");
13243 assert!(is_cjk_break_allowed_by_strictness(ch, None, Loose), "{ch:?}");
13244 }
13245
13246 for level in [Normal, Auto] {
13248 assert!(!is_cjk_break_allowed_by_strictness('\u{2010}', None, level));
13249 assert!(!is_cjk_break_allowed_by_strictness('\u{2013}', None, level));
13250 assert!(is_cjk_break_allowed_by_strictness('っ', None, level));
13251 assert!(is_cjk_break_allowed_by_strictness('中', None, level));
13252 }
13253
13254 assert!(!is_cjk_break_allowed_by_strictness('っ', None, Strict));
13256 assert!(!is_cjk_break_allowed_by_strictness('ー', None, Strict));
13257 assert!(!is_cjk_break_allowed_by_strictness('\u{301C}', None, Strict));
13258 assert!(!is_cjk_break_allowed_by_strictness('\u{30A0}', None, Strict));
13259 assert!(is_cjk_break_allowed_by_strictness('中', None, Strict));
13260
13261 assert_eq!(
13263 is_cjk_break_allowed_by_strictness('中', Some('x'), Strict),
13264 is_cjk_break_allowed_by_strictness('中', None, Strict)
13265 );
13266 }
13267
13268 fn plain_glyph(codepoint: char, advance: f32) -> Glyph {
13273 Glyph {
13274 glyph_id: 1,
13275 codepoint,
13276 font_hash: 7,
13277 font_metrics: std_metrics(),
13278 style: style(),
13279 source: GlyphSource::Char,
13280 logical_byte_index: 0,
13281 logical_byte_len: codepoint.len_utf8(),
13282 content_index: 0,
13283 cluster: 0,
13284 advance,
13285 kerning: 0.0,
13286 offset: Point { x: 0.0, y: 0.0 },
13287 vertical_advance: advance,
13288 vertical_origin_y: 0.0,
13289 vertical_bearing: Point { x: 0.0, y: 0.0 },
13290 orientation: GlyphOrientation::Horizontal,
13291 script: Script::Latin,
13292 bidi_level: BidiLevel::new(0),
13293 }
13294 }
13295
13296 #[test]
13297 fn glyph_bounds_is_advance_by_resolved_line_height() {
13298 let g = plain_glyph('a', 9.5);
13299 let b = g.bounds();
13300 assert_eq!(b.x, 0.0);
13301 assert_eq!(b.y, 0.0);
13302 assert_eq!(b.width, 9.5);
13303 approx(b.height, 16.0); }
13305
13306 #[test]
13307 fn glyph_bounds_with_zero_advance_and_zero_upem_does_not_panic() {
13308 let mut g = plain_glyph('a', 0.0);
13309 g.font_metrics = metrics(0, 0.0, 0.0, 0.0);
13310 let b = g.bounds();
13311 assert_eq!(b.width, 0.0);
13312 approx(b.height, 19.2); }
13314
13315 #[test]
13316 fn glyph_whitespace_and_justification_predicates() {
13317 let space = plain_glyph(' ', 4.0);
13318 assert!(space.is_whitespace());
13319 assert_eq!(space.character_class(), CharacterClass::Space);
13320 assert!(!space.can_justify(), "whitespace is never itself justified");
13321 assert_eq!(space.justification_priority(), 0);
13322 assert!(space.break_opportunity_after());
13323
13324 let letter = plain_glyph('a', 8.0);
13325 assert!(!letter.is_whitespace());
13326 assert!(letter.can_justify());
13327 assert_eq!(letter.justification_priority(), 192);
13328 assert!(!letter.break_opportunity_after());
13329
13330 let combining = plain_glyph('\u{0301}', 0.0);
13331 assert!(!combining.is_whitespace());
13332 assert!(!combining.can_justify(), "combining marks are never justified");
13333 assert_eq!(combining.justification_priority(), 255);
13334 }
13335
13336 #[test]
13337 fn glyph_break_opportunity_after_covers_every_hyphen_form() {
13338 assert!(plain_glyph('\u{00AD}', 0.0).break_opportunity_after(), "soft hyphen");
13339 assert!(plain_glyph('\u{002D}', 4.0).break_opportunity_after(), "hyphen-minus");
13340 assert!(plain_glyph('\u{2010}', 4.0).break_opportunity_after(), "U+2010");
13341 assert!(plain_glyph('\t', 8.0).break_opportunity_after(), "tab is whitespace");
13342 assert!(!plain_glyph('\u{2011}', 4.0).break_opportunity_after(), "NON-BREAKING hyphen");
13343 assert!(!plain_glyph('/', 4.0).break_opportunity_after());
13344 }
13345
13346 #[test]
13351 fn shaped_item_as_cluster_only_matches_clusters() {
13352 assert!(cl("a", 8.0).as_cluster().is_some());
13353 assert!(obj(10.0, 10.0, 0.0).as_cluster().is_none());
13354 assert!(brk().as_cluster().is_none());
13355 assert!(tab(8.0, 16.0).as_cluster().is_none());
13356 }
13357
13358 #[test]
13359 fn shaped_item_bounds_of_a_break_is_the_zero_rect() {
13360 assert_eq!(brk().bounds(), Rect::default());
13361 assert_eq!(obj(10.0, 20.0, 0.0).bounds().width, 10.0);
13362 assert_eq!(tab(8.0, 16.0).bounds().height, 16.0);
13363 let c = cl("a", 9.5);
13364 assert_eq!(c.bounds().width, 9.5, "a cluster's width is its advance");
13365 approx(c.bounds().height, 16.0); }
13367
13368 #[test]
13369 fn get_item_measure_sums_advance_and_kerning() {
13370 let st = style();
13371 let mut g1 = shaped_glyph(st.clone(), std_metrics(), 8.0);
13372 g1.kerning = -1.5;
13373 let mut g2 = shaped_glyph(st.clone(), std_metrics(), 8.0);
13374 g2.kerning = 0.5;
13375 let item = make_cluster("ab", 16.0, st, smallvec![g1, g2], gid(0, 0));
13376 assert_eq!(get_item_measure(&item, false), 15.0, "16 + (-1.5) + 0.5");
13377 assert_eq!(
13378 get_item_measure(&item, true),
13379 15.0,
13380 "clusters ignore the is_vertical flag (advance is already axis-relative)"
13381 );
13382 }
13383
13384 #[test]
13385 fn get_item_measure_of_a_break_is_zero_and_objects_switch_axis() {
13386 assert_eq!(get_item_measure(&brk(), false), 0.0);
13387 assert_eq!(get_item_measure(&brk(), true), 0.0);
13388 let o = obj(30.0, 20.0, 0.0);
13389 assert_eq!(get_item_measure(&o, false), 30.0);
13390 assert_eq!(get_item_measure(&o, true), 20.0);
13391 }
13392
13393 #[test]
13394 fn get_item_measure_with_spacing_adds_letter_spacing_but_not_for_cursive() {
13395 let st = styled(|s| s.letter_spacing = Spacing::PxF(2.0));
13396 let latin = cl_styled("a", 10.0, st.clone());
13397 assert_eq!(get_item_measure(&latin, false), 10.0);
13398 assert_eq!(get_item_measure_with_spacing(&latin, false), 12.0);
13399
13400 let arabic = cl_styled("\u{0627}", 10.0, st);
13402 assert_eq!(
13403 get_item_measure_with_spacing(&arabic, false),
13404 10.0,
13405 "letter-spacing is suppressed for cursive scripts (CSS Text 3 App. D)"
13406 );
13407 }
13408
13409 #[test]
13410 fn get_item_measure_with_spacing_adds_word_spacing_only_on_separators() {
13411 let st = styled(|s| {
13412 s.word_spacing = Spacing::PxF(5.0);
13413 s.letter_spacing = Spacing::PxF(1.0);
13414 });
13415 let space = cl_styled(" ", 4.0, st.clone());
13416 assert_eq!(
13417 get_item_measure_with_spacing(&space, false),
13418 10.0,
13419 "4 + letter(1) + word(5)"
13420 );
13421 let letter = cl_styled("a", 8.0, st);
13422 assert_eq!(
13423 get_item_measure_with_spacing(&letter, false),
13424 9.0,
13425 "no word-spacing on a non-separator"
13426 );
13427 assert_eq!(get_item_measure_with_spacing(&brk(), false), 0.0);
13429 }
13430
13431 #[test]
13432 fn is_collapsible_whitespace_is_vacuously_true_for_an_empty_cluster() {
13433 assert!(is_collapsible_whitespace(&cl(" ", 4.0)));
13434 assert!(is_collapsible_whitespace(&cl("\t", 8.0)));
13435 assert!(is_collapsible_whitespace(&cl("\u{1680}", 4.0)));
13436 assert!(is_collapsible_whitespace(&cl(" \t ", 16.0)));
13437 assert!(!is_collapsible_whitespace(&cl("\n", 0.0)), "newline is NOT collapsible here");
13438 assert!(!is_collapsible_whitespace(&cl("a", 8.0)));
13439 assert!(!is_collapsible_whitespace(&cl("a ", 12.0)), "all() — mixed is false");
13440 assert!(!is_collapsible_whitespace(&obj(1.0, 1.0, 0.0)));
13441 assert!(
13444 is_collapsible_whitespace(&cl("", 0.0)),
13445 "an empty cluster counts as collapsible whitespace"
13446 );
13447 }
13448
13449 #[test]
13450 fn is_word_separator_and_zero_width_space_on_items() {
13451 assert!(is_word_separator(&cl(" ", 4.0)));
13452 assert!(is_word_separator(&cl("a b", 20.0)), "any() — one space suffices");
13453 assert!(!is_word_separator(&cl("", 0.0)), "any() on empty is false");
13454 assert!(!is_word_separator(&cl("\u{3000}", 16.0)));
13455 assert!(!is_word_separator(&brk()));
13456 assert!(!is_word_separator(&obj(1.0, 1.0, 0.0)));
13457
13458 assert!(is_zero_width_space(&cl("\u{200B}", 0.0)));
13459 assert!(is_zero_width_space(&cl("a\u{200B}", 8.0)), "contains(), not equals()");
13460 assert!(!is_zero_width_space(&cl(" ", 4.0)));
13461 assert!(!is_zero_width_space(&obj(1.0, 1.0, 0.0)));
13462 }
13463
13464 #[test]
13465 fn can_justify_after_rejects_objects_empty_clusters_and_combining_marks() {
13466 assert!(can_justify_after(&cl("a", 8.0)));
13467 assert!(!can_justify_after(&cl(" ", 4.0)));
13468 assert!(!can_justify_after(&cl("a\u{0301}", 8.0)), "trailing combining mark");
13469 assert!(!can_justify_after(&cl("", 0.0)), "no last char → false");
13470 assert!(
13471 !can_justify_after(&obj(10.0, 10.0, 0.0)),
13472 "CSS 2.2 §9.4.2: never stretch after an atomic inline"
13473 );
13474 assert!(!can_justify_after(&brk()));
13475 }
13476
13477 #[test]
13478 fn is_hanging_punctuation_requires_a_single_glyph_cluster() {
13479 assert!(is_hanging_punctuation(&cl(".", 4.0)));
13480 assert!(is_hanging_punctuation(&cl(",", 4.0)));
13481 assert!(!is_hanging_punctuation(&cl("a", 8.0)));
13482 assert!(!is_hanging_punctuation(&cl("", 0.0)), "no first char");
13483 assert!(!is_hanging_punctuation(&obj(1.0, 1.0, 0.0)));
13484
13485 let st = style();
13487 let g1 = shaped_glyph(st.clone(), std_metrics(), 4.0);
13488 let g2 = shaped_glyph(st.clone(), std_metrics(), 4.0);
13489 let two = make_cluster(".", 8.0, st, smallvec![g1, g2], gid(0, 0));
13490 assert!(!is_hanging_punctuation(&two));
13491 }
13492
13493 #[test]
13494 fn cluster_script_predicates() {
13495 let arabic = cl("\u{0627}", 10.0);
13496 let latin = cl("a", 8.0);
13497 let cjk = cl("中", 16.0);
13498
13499 assert!(is_cursive_script_cluster(arabic.as_cluster().unwrap()));
13500 assert!(!is_cursive_script_cluster(latin.as_cluster().unwrap()));
13501 assert!(
13502 !is_cursive_script_cluster(cl("", 0.0).as_cluster().unwrap()),
13503 "empty cluster has no first char"
13504 );
13505
13506 assert!(is_cjk_cluster(cjk.as_cluster().unwrap()));
13507 assert!(!is_cjk_cluster(latin.as_cluster().unwrap()));
13508
13509 assert!(
13511 !is_arabic_cluster(arabic.as_cluster().unwrap()),
13512 "the fixture's glyph carries Script::Latin, so the text alone is not enough"
13513 );
13514 let st = style();
13515 let mut g = shaped_glyph(st.clone(), std_metrics(), 10.0);
13516 g.script = Script::Arabic;
13517 let real_arabic = make_cluster("\u{0627}", 10.0, st, smallvec![g], gid(0, 0));
13518 assert!(is_arabic_cluster(real_arabic.as_cluster().unwrap()));
13519 }
13520
13521 #[test]
13522 fn cluster_is_word_boundary_for_punctuation_and_whitespace() {
13523 assert!(cluster_is_word_boundary(cl(" ", 4.0).as_cluster().unwrap()));
13524 assert!(cluster_is_word_boundary(cl(".", 4.0).as_cluster().unwrap()));
13525 assert!(cluster_is_word_boundary(cl("", 0.0).as_cluster().unwrap()), "vacuous");
13526 assert!(!cluster_is_word_boundary(cl("a", 8.0).as_cluster().unwrap()));
13527 assert!(!cluster_is_word_boundary(cl("_", 8.0).as_cluster().unwrap()));
13528 }
13529
13530 #[test]
13531 fn get_baseline_for_item_only_defined_for_clusters_and_boxes() {
13532 assert_eq!(get_baseline_for_item(&brk()), None);
13533 assert_eq!(get_baseline_for_item(&tab(8.0, 16.0)), None);
13534 assert_eq!(get_baseline_for_item(&obj(10.0, 20.0, 3.0)), Some(3.0));
13535 approx(
13537 get_baseline_for_item(&cl("a", 8.0)).expect("a glyph-bearing cluster has a baseline"),
13538 12.8,
13539 );
13540 assert_eq!(
13541 get_baseline_for_item(&cl_no_glyphs("", 0.0)),
13542 None,
13543 "a glyph-less cluster has no baseline"
13544 );
13545 }
13546
13547 #[test]
13548 fn get_item_vertical_metrics_approx_for_every_variant() {
13549 let (a, d) = get_item_vertical_metrics_approx(&cl("a", 8.0));
13551 approx(a, 12.8);
13552 approx(d, 3.2);
13553
13554 let (a, d) = get_item_vertical_metrics_approx(&cl_no_glyphs("", 0.0));
13556 approx(a, 19.2 * FALLBACK_ASCENT_RATIO);
13557 approx(d, 19.2 * FALLBACK_DESCENT_RATIO);
13558
13559 assert_eq!(get_item_vertical_metrics_approx(&obj(10.0, 20.0, 5.0)), (20.0, 0.0));
13561 assert_eq!(get_item_vertical_metrics_approx(&brk()), (0.0, 0.0));
13563 let (a, d) = get_item_vertical_metrics_approx(&tab(8.0, 10.0));
13565 approx(a, 8.0);
13566 approx(d, 2.0);
13567 }
13568
13569 #[test]
13570 fn get_item_vertical_metrics_approx_skips_zero_upem_glyphs() {
13571 let st = style();
13572 let g = shaped_glyph(st.clone(), metrics(0, 800.0, -200.0, 0.0), 8.0);
13573 let item = make_cluster("a", 8.0, st, smallvec![g], gid(0, 0));
13574 assert_eq!(
13575 get_item_vertical_metrics_approx(&item),
13576 (0.0, 0.0),
13577 "a zero-upem glyph is skipped rather than producing inf/NaN metrics"
13578 );
13579 }
13580
13581 #[test]
13582 fn get_item_vertical_metrics_uses_the_strut_for_glyphless_clusters() {
13583 let c = UnifiedConstraints::default();
13584 let (a, d) = get_item_vertical_metrics(&cl_no_glyphs("", 0.0), &c);
13585 approx(a, DEFAULT_STRUT_ASCENT + 1.6);
13587 approx(d, DEFAULT_STRUT_DESCENT + 1.6);
13588
13589 assert_eq!(get_item_vertical_metrics(&brk(), &c), (0.0, 0.0));
13590 let (a, d) = get_item_vertical_metrics(&obj(10.0, 10.0, 30.0), &c);
13592 assert_eq!(a, 0.0, "baseline_offset > height must clamp the ascent at 0");
13593 assert_eq!(d, 30.0);
13594 }
13595
13596 #[test]
13597 fn get_item_vertical_align_only_for_objects_with_image_or_shape_content() {
13598 assert_eq!(get_item_vertical_align(&cl("a", 8.0)), None);
13599 assert_eq!(get_item_vertical_align(&brk()), None);
13600 assert_eq!(get_item_vertical_align(&obj(1.0, 1.0, 0.0)), None);
13602
13603 let img = ShapedItem::Object {
13604 source: ci(0, 0),
13605 bounds: Rect::default(),
13606 baseline_offset: 0.0,
13607 content: InlineContent::Image(InlineImage {
13608 source: ImageSource::Placeholder(Size::new(10.0, 10.0)),
13609 intrinsic_size: Size::new(10.0, 10.0),
13610 display_size: None,
13611 baseline_offset: 0.0,
13612 alignment: VerticalAlign::Top,
13613 object_fit: ObjectFit::Fill,
13614 }),
13615 };
13616 assert_eq!(get_item_vertical_align(&img), Some(VerticalAlign::Top));
13617 }
13618
13619 #[test]
13624 fn no_break_space_is_a_word_separator_but_never_a_break_opportunity() {
13625 let nbsp = cl("\u{00A0}", 4.0);
13626 assert!(is_word_separator( ), "NBSP participates in word-spacing");
13627 assert!(
13628 !is_break_opportunity( ),
13629 "...but must NOT offer a soft wrap (10\\u{{00A0}}km must not wrap)"
13630 );
13631 assert!(!is_break_opportunity_with_word_break(
13632  ,
13633 WordBreak::BreakAll,
13634 Hyphens::Auto
13635 ));
13636 for ch in ['\u{202F}', '\u{2060}', '\u{FEFF}'] {
13638 let item = cl(&ch.to_string(), 4.0);
13639 assert!(
13640 !is_break_opportunity_with_word_break(&item, WordBreak::BreakAll, Hyphens::Auto),
13641 "{ch:?} must suppress breaks"
13642 );
13643 }
13644 }
13645
13646 #[test]
13647 fn zero_width_space_breaks_even_under_keep_all() {
13648 let zwsp = cl("\u{200B}", 0.0);
13649 assert!(is_break_opportunity(&zwsp));
13650 for wb in [WordBreak::Normal, WordBreak::BreakAll, WordBreak::KeepAll] {
13651 assert!(
13652 is_break_opportunity_with_word_break(&zwsp, wb, Hyphens::None),
13653 "ZWSP must always break ({wb:?})"
13654 );
13655 }
13656 }
13657
13658 #[test]
13659 fn word_break_modes_change_cjk_break_opportunities() {
13660 let cjk = cl("中", 16.0);
13661 assert!(is_break_opportunity_with_word_break(&cjk, WordBreak::Normal, Hyphens::Manual));
13662 assert!(is_break_opportunity_with_word_break(&cjk, WordBreak::BreakAll, Hyphens::Manual));
13663 assert!(
13664 !is_break_opportunity_with_word_break(&cjk, WordBreak::KeepAll, Hyphens::Manual),
13665 "keep-all suppresses inter-ideograph breaks"
13666 );
13667
13668 let latin = cl("a", 8.0);
13669 assert!(!is_break_opportunity_with_word_break(&latin, WordBreak::Normal, Hyphens::Manual));
13670 assert!(
13671 is_break_opportunity_with_word_break(&latin, WordBreak::BreakAll, Hyphens::Manual),
13672 "break-all makes every cluster breakable"
13673 );
13674 assert!(!is_break_opportunity_with_word_break(&latin, WordBreak::KeepAll, Hyphens::Manual));
13675 }
13676
13677 #[test]
13678 fn soft_hyphen_break_depends_on_the_hyphens_property() {
13679 let shy = cl("\u{00AD}", 0.0);
13680 assert!(!is_break_opportunity_with_word_break(
13681 ­,
13682 WordBreak::Normal,
13683 Hyphens::None
13684 ));
13685 assert!(is_break_opportunity_with_word_break(
13686 ­,
13687 WordBreak::Normal,
13688 Hyphens::Manual
13689 ));
13690 assert!(is_break_opportunity_with_word_break(
13691 ­,
13692 WordBreak::Normal,
13693 Hyphens::Auto
13694 ));
13695 }
13696
13697 #[test]
13698 fn trailing_hyphen_and_slash_always_break_regardless_of_hyphens() {
13699 for text in ["co-", "co\u{2010}", "a/"] {
13700 let item = cl(text, 10.0);
13701 assert!(
13702 is_break_opportunity_with_word_break(&item, WordBreak::KeepAll, Hyphens::None),
13703 "{text:?} must offer a break after it even with hyphens:none"
13704 );
13705 }
13706 assert!(!is_break_opportunity_with_word_break(
13708 &cl("-a", 10.0),
13709 WordBreak::Normal,
13710 Hyphens::None
13711 ));
13712 }
13713
13714 #[test]
13715 fn atomic_inlines_are_break_opportunities_but_breaks_and_tabs_differ() {
13716 assert!(is_break_opportunity(&obj(10.0, 10.0, 0.0)), "CSS Text 3 §5.1");
13717 assert!(is_break_opportunity(&brk()));
13718 assert!(!is_break_opportunity(&tab(8.0, 16.0)), "a Tab is not itself a wrap point");
13719 assert!(is_break_opportunity(&cl(" ", 4.0)));
13720 assert!(!is_break_opportunity(&cl("a", 8.0)));
13721 }
13722
13723 #[test]
13728 fn merge_segments_of_zero_or_one_segment_is_identity() {
13729 assert!(merge_segments(Vec::new()).is_empty());
13730 let one = vec![LineSegment {
13731 start_x: 5.0,
13732 width: 3.0,
13733 priority: 0,
13734 }];
13735 let out = merge_segments(one);
13736 assert_eq!(out.len(), 1);
13737 assert_eq!(out[0].start_x, 5.0);
13738 }
13739
13740 #[test]
13741 fn merge_segments_joins_overlapping_and_touching_spans() {
13742 let segs = vec![
13743 LineSegment {
13744 start_x: 0.0,
13745 width: 10.0,
13746 priority: 0,
13747 },
13748 LineSegment {
13749 start_x: 5.0,
13750 width: 10.0,
13751 priority: 0,
13752 }, LineSegment {
13754 start_x: 15.0,
13755 width: 5.0,
13756 priority: 0,
13757 }, LineSegment {
13759 start_x: 100.0,
13760 width: 5.0,
13761 priority: 0,
13762 }, ];
13764 let out = merge_segments(segs);
13765 assert_eq!(out.len(), 2, "three touching spans collapse into one");
13766 assert_eq!(out[0].start_x, 0.0);
13767 assert_eq!(out[0].width, 20.0);
13768 assert_eq!(out[1].start_x, 100.0);
13769 }
13770
13771 #[test]
13772 fn merge_segments_sorts_unordered_input() {
13773 let segs = vec![
13774 LineSegment {
13775 start_x: 50.0,
13776 width: 5.0,
13777 priority: 0,
13778 },
13779 LineSegment {
13780 start_x: 0.0,
13781 width: 5.0,
13782 priority: 0,
13783 },
13784 ];
13785 let out = merge_segments(segs);
13786 assert_eq!(out.len(), 2);
13787 assert_eq!(out[0].start_x, 0.0);
13788 assert_eq!(out[1].start_x, 50.0);
13789 }
13790
13791 #[test]
13792 fn merge_segments_is_nan_tolerant() {
13793 let segs = vec![
13797 LineSegment {
13798 start_x: 0.0,
13799 width: 10.0,
13800 priority: 0,
13801 },
13802 LineSegment {
13803 start_x: f32::NAN,
13804 width: 10.0,
13805 priority: 0,
13806 },
13807 ];
13808 let out = merge_segments(segs);
13809 assert!(!out.is_empty(), "NaN input must not abort the merge");
13810 }
13811
13812 #[test]
13813 fn polygon_line_intersection_needs_at_least_three_points() {
13814 assert!(polygon_line_intersection(&[], 0.0, 1.0).is_empty());
13815 assert!(polygon_line_intersection(&[Point { x: 0.0, y: 0.0 }], 0.0, 1.0).is_empty());
13816 assert!(polygon_line_intersection(
13817 &[Point { x: 0.0, y: 0.0 }, Point { x: 1.0, y: 1.0 }],
13818 0.0,
13819 1.0
13820 )
13821 .is_empty());
13822 }
13823
13824 #[test]
13825 fn polygon_line_intersection_narrows_across_a_triangle() {
13826 let tri = [
13828 Point { x: 0.0, y: 0.0 },
13829 Point { x: 100.0, y: 0.0 },
13830 Point { x: 0.0, y: 100.0 },
13831 ];
13832 let top = polygon_line_intersection(&tri, 10.0, 1.0);
13833 let bot = polygon_line_intersection(&tri, 80.0, 1.0);
13834 assert_eq!(top.len(), 1);
13835 assert_eq!(bot.len(), 1);
13836 assert!(
13837 top[0].width > bot[0].width,
13838 "the band must narrow with y ({} !> {})",
13839 top[0].width,
13840 bot[0].width
13841 );
13842 assert!((top[0].width - 89.5).abs() < 1.0);
13843 }
13844
13845 #[test]
13846 fn polygon_line_intersection_outside_the_shape_and_on_nan_scanlines_is_empty() {
13847 let tri = [
13848 Point { x: 0.0, y: 0.0 },
13849 Point { x: 100.0, y: 0.0 },
13850 Point { x: 0.0, y: 100.0 },
13851 ];
13852 assert!(
13853 polygon_line_intersection(&tri, 500.0, 1.0).is_empty(),
13854 "a scanline below the shape yields no spans"
13855 );
13856 assert!(
13857 polygon_line_intersection(&tri, f32::NAN, 1.0).is_empty(),
13858 "a NaN scanline must not panic — every crossing test is false"
13859 );
13860 assert!(polygon_line_intersection(&tri, f32::INFINITY, 1.0).is_empty());
13861 }
13862
13863 #[test]
13864 fn polygon_line_intersection_of_a_degenerate_flat_polygon_is_empty() {
13865 let flat = [
13867 Point { x: 0.0, y: 5.0 },
13868 Point { x: 10.0, y: 5.0 },
13869 Point { x: 20.0, y: 5.0 },
13870 ];
13871 assert!(polygon_line_intersection(&flat, 4.5, 1.0).is_empty());
13872 }
13873
13874 #[test]
13875 fn path_segments_line_intersection_on_empty_and_degenerate_input() {
13876 assert!(path_segments_line_intersection(&[], 0.0, 1.0).is_empty());
13877 assert!(path_segments_line_intersection(
13879 &[PathSegment::MoveTo(Point { x: 0.0, y: 0.0 })],
13880 0.0,
13881 1.0
13882 )
13883 .is_empty());
13884 }
13885
13886 #[test]
13887 fn path_segments_line_intersection_of_a_square() {
13888 let sq = vec![
13889 PathSegment::MoveTo(Point { x: 0.0, y: 0.0 }),
13890 PathSegment::LineTo(Point { x: 100.0, y: 0.0 }),
13891 PathSegment::LineTo(Point {
13892 x: 100.0,
13893 y: 100.0,
13894 }),
13895 PathSegment::LineTo(Point { x: 0.0, y: 100.0 }),
13896 PathSegment::Close,
13897 ];
13898 let spans = path_segments_line_intersection(&sq, 50.0, 1.0);
13899 assert_eq!(spans.len(), 1);
13900 assert!((spans[0].0 - 0.0).abs() < 0.01);
13901 assert!((spans[0].1 - 100.0).abs() < 0.01);
13902 assert!(path_segments_line_intersection(&sq, 500.0, 1.0).is_empty());
13904 }
13905
13906 #[test]
13907 fn get_shape_horizontal_spans_rectangle_only_when_the_line_box_overlaps() {
13908 let r = ShapeBoundary::Rectangle(Rect {
13909 x: 10.0,
13910 y: 20.0,
13911 width: 30.0,
13912 height: 40.0,
13913 });
13914 assert_eq!(get_shape_horizontal_spans(&r, 30.0, 10.0), vec![(10.0, 40.0)]);
13915 assert!(get_shape_horizontal_spans(&r, 0.0, 10.0).is_empty(), "above");
13916 assert!(get_shape_horizontal_spans(&r, 100.0, 10.0).is_empty(), "below");
13917 assert!(get_shape_horizontal_spans(&r, 10.0, 10.0).is_empty());
13919 let flat = ShapeBoundary::Rectangle(Rect {
13921 x: 0.0,
13922 y: 0.0,
13923 width: 10.0,
13924 height: 0.0,
13925 });
13926 assert!(get_shape_horizontal_spans(&flat, 0.0, 10.0).is_empty());
13927 }
13928
13929 #[test]
13930 fn get_shape_horizontal_spans_circle_edges_and_zero_radius() {
13931 let c = ShapeBoundary::Circle {
13932 center: Point { x: 50.0, y: 50.0 },
13933 radius: 10.0,
13934 };
13935 let mid = get_shape_horizontal_spans(&c, 49.5, 1.0); assert_eq!(mid.len(), 1);
13937 assert!((mid[0].0 - 40.0).abs() < 0.01);
13938 assert!((mid[0].1 - 60.0).abs() < 0.01);
13939 assert!(get_shape_horizontal_spans(&c, 1000.0, 1.0).is_empty());
13940
13941 let dot = ShapeBoundary::Circle {
13943 center: Point { x: 5.0, y: 5.0 },
13944 radius: 0.0,
13945 };
13946 let spans = get_shape_horizontal_spans(&dot, 4.5, 1.0);
13947 assert_eq!(spans, vec![(5.0, 5.0)], "degenerate but not a panic");
13948 }
13949
13950 #[test]
13951 fn get_shape_horizontal_spans_ellipse_with_zero_radii_is_empty_not_a_panic() {
13952 let e = ShapeBoundary::Ellipse {
13954 center: Point { x: 0.0, y: 0.0 },
13955 radii: Size::zero(),
13956 };
13957 assert!(
13958 get_shape_horizontal_spans(&e, 0.0, 1.0).is_empty(),
13959 "a zero-sized ellipse divides by zero but must not panic"
13960 );
13961 }
13962
13963 #[test]
13964 fn get_shape_horizontal_spans_polygon_delegates_to_the_scanline() {
13965 let p = ShapeBoundary::Polygon {
13966 points: vec![
13967 Point { x: 0.0, y: 0.0 },
13968 Point { x: 100.0, y: 0.0 },
13969 Point { x: 0.0, y: 100.0 },
13970 ],
13971 };
13972 let spans = get_shape_horizontal_spans(&p, 10.0, 1.0);
13973 assert_eq!(spans.len(), 1);
13974 assert!(spans[0].1 > spans[0].0);
13975 }
13976
13977 #[test]
13982 fn extract_line_breaks_of_no_items_is_empty_but_keeps_the_width() {
13983 let lb = extract_line_breaks(&[], 640.0);
13984 assert!(lb.line_ranges.is_empty());
13985 assert!(lb.line_widths.is_empty());
13986 assert_eq!(lb.available_width, 640.0);
13987 let nan = extract_line_breaks(&[], f32::NAN);
13989 assert!(nan.available_width.is_nan());
13990 }
13991
13992 #[test]
13993 fn extract_line_breaks_groups_items_by_line_index() {
13994 let items = vec![
13995 pos(cl("a", 10.0), 0.0, 0.0, 0),
13996 pos(cl("b", 10.0), 10.0, 0.0, 0),
13997 pos(cl("c", 10.0), 0.0, 20.0, 1),
13998 ];
13999 let lb = extract_line_breaks(&items, 100.0);
14000 assert_eq!(lb.line_ranges, vec![(0, 2), (2, 3)]);
14001 assert_eq!(lb.line_widths, vec![20.0, 10.0]);
14002 assert_eq!(lb.line_ranges.len(), lb.line_widths.len());
14003 }
14004
14005 #[test]
14006 fn extract_line_breaks_splits_on_every_line_index_change_even_going_backwards() {
14007 let items = vec![
14010 pos(cl("a", 10.0), 0.0, 0.0, 0),
14011 pos(cl("b", 10.0), 0.0, 20.0, 1),
14012 pos(cl("c", 10.0), 0.0, 0.0, 0),
14013 ];
14014 let lb = extract_line_breaks(&items, 100.0);
14015 assert_eq!(lb.line_ranges.len(), 3);
14016 assert_eq!(lb.line_widths, vec![10.0, 10.0, 10.0]);
14017 }
14018
14019 #[test]
14020 fn try_incremental_relayout_no_dirty_items_is_a_glyph_swap() {
14021 let lb = CachedLineBreaks {
14022 line_ranges: vec![(0, 2)],
14023 line_widths: vec![20.0],
14024 available_width: 100.0,
14025 };
14026 assert!(matches!(
14027 try_incremental_relayout(&[], &[10.0, 10.0], &[10.0, 10.0], &lb),
14028 IncrementalRelayoutResult::GlyphSwap
14029 ));
14030 }
14031
14032 #[test]
14033 fn try_incremental_relayout_out_of_range_dirty_index_falls_back_to_full() {
14034 let lb = CachedLineBreaks {
14035 line_ranges: vec![(0, 2)],
14036 line_widths: vec![20.0],
14037 available_width: 100.0,
14038 };
14039 assert!(matches!(
14040 try_incremental_relayout(&[99], &[10.0, 10.0], &[10.0, 10.0], &lb),
14041 IncrementalRelayoutResult::FullRelayout
14042 ));
14043 assert!(
14044 matches!(
14045 try_incremental_relayout(&[usize::MAX], &[10.0], &[10.0], &lb),
14046 IncrementalRelayoutResult::FullRelayout
14047 ),
14048 "usize::MAX must not index-panic"
14049 );
14050 assert!(matches!(
14052 try_incremental_relayout(&[1], &[10.0, 10.0], &[10.0], &lb),
14053 IncrementalRelayoutResult::FullRelayout
14054 ));
14055 }
14056
14057 #[test]
14058 fn try_incremental_relayout_same_width_is_a_glyph_swap() {
14059 let lb = CachedLineBreaks {
14060 line_ranges: vec![(0, 2)],
14061 line_widths: vec![20.0],
14062 available_width: 100.0,
14063 };
14064 assert!(matches!(
14066 try_incremental_relayout(&[0], &[10.0, 10.0], &[10.0005, 10.0], &lb),
14067 IncrementalRelayoutResult::GlyphSwap
14068 ));
14069 }
14070
14071 #[test]
14072 fn try_incremental_relayout_shifts_when_it_still_fits_and_reflows_when_it_does_not() {
14073 let lb = CachedLineBreaks {
14074 line_ranges: vec![(0, 2), (2, 4)],
14075 line_widths: vec![20.0, 20.0],
14076 available_width: 100.0,
14077 };
14078 let old = [10.0, 10.0, 10.0, 10.0];
14079
14080 let grew = [10.0, 30.0, 10.0, 10.0]; match try_incremental_relayout(&[1], &old, &grew, &lb) {
14082 IncrementalRelayoutResult::LineShift {
14083 affected_item,
14084 delta,
14085 } => {
14086 assert_eq!(affected_item, 1);
14087 assert_eq!(delta, 20.0);
14088 }
14089 other => panic!("expected LineShift, got {other:?}"),
14090 }
14091
14092 let exploded = [10.0, 10.0, 10.0, 500.0]; match try_incremental_relayout(&[3], &old, &exploded, &lb) {
14094 IncrementalRelayoutResult::PartialReflow { reflow_from_line } => {
14095 assert_eq!(reflow_from_line, 1);
14096 }
14097 other => panic!("expected PartialReflow, got {other:?}"),
14098 }
14099 }
14100
14101 #[test]
14102 fn try_incremental_relayout_dirty_item_outside_every_line_range_is_a_full_relayout() {
14103 let lb = CachedLineBreaks {
14104 line_ranges: vec![(0, 1)],
14105 line_widths: vec![10.0],
14106 available_width: 100.0,
14107 };
14108 assert!(matches!(
14110 try_incremental_relayout(&[1], &[10.0, 10.0], &[10.0, 50.0], &lb),
14111 IncrementalRelayoutResult::FullRelayout
14112 ));
14113 }
14114
14115 #[test]
14116 fn try_incremental_relayout_with_nan_advances_reflows_rather_than_shifting() {
14117 let lb = CachedLineBreaks {
14118 line_ranges: vec![(0, 1)],
14119 line_widths: vec![10.0],
14120 available_width: 100.0,
14121 };
14122 match try_incremental_relayout(&[0], &[10.0], &[f32::NAN], &lb) {
14125 IncrementalRelayoutResult::PartialReflow { reflow_from_line } => {
14126 assert_eq!(reflow_from_line, 0);
14127 }
14128 other => panic!("NaN advance should reflow, got {other:?}"),
14129 }
14130 }
14131
14132 #[test]
14137 fn text_cache_memory_report_total_bytes_sums_only_the_byte_fields() {
14138 let r = TextCacheMemoryReport::default();
14139 assert_eq!(r.total_bytes(), 0);
14140
14141 let full = TextCacheMemoryReport {
14142 logical_items_entries: 1_000_000, logical_items_bytes: 1,
14144 visual_items_entries: 1_000_000, visual_items_bytes: 2,
14146 shaped_items_entries: 1_000_000, shaped_items_bytes: 4,
14148 shaped_glyph_bytes: 8,
14149 shaped_cluster_text_bytes: 16,
14150 per_item_shaped_entries: 1_000_000, per_item_shaped_bytes: 32,
14152 };
14153 assert_eq!(full.total_bytes(), 63, "1+2+4+8+16+32");
14154 }
14155
14156 #[test]
14157 fn text_shaping_cache_new_is_empty_and_reports_zero_bytes() {
14158 let c = TextShapingCache::new();
14159 let r = c.memory_report();
14160 assert_eq!(r.total_bytes(), 0);
14161 assert_eq!(r.logical_items_entries, 0);
14162 assert_eq!(r.per_item_shaped_entries, 0);
14163 assert_eq!(c.generation, 0);
14164 let d = TextShapingCache::default();
14166 assert_eq!(d.memory_report().total_bytes(), 0);
14167 }
14168
14169 #[test]
14170 fn text_shaping_cache_begin_generation_is_idempotent_on_an_empty_cache() {
14171 let mut c = TextShapingCache::new();
14172 for expect in 1..=5_u64 {
14173 c.begin_generation();
14174 assert_eq!(c.generation, expect);
14175 }
14176 assert!(c.per_item_accessed.is_empty());
14177 assert!(c.per_item_shaped.is_empty());
14178 }
14179
14180 #[test]
14181 fn text_shaping_cache_begin_generation_evicts_unaccessed_per_item_entries() {
14182 let mut c = TextShapingCache::new();
14183 c.per_item_shaped.insert(
14184 1,
14185 Arc::new(PerItemShapedEntry {
14186 clusters: vec![cl("a", 8.0)],
14187 total_advance: 8.0,
14188 }),
14189 );
14190 c.per_item_shaped.insert(
14191 2,
14192 Arc::new(PerItemShapedEntry {
14193 clusters: Vec::new(),
14194 total_advance: 0.0,
14195 }),
14196 );
14197 c.begin_generation();
14199 assert_eq!(c.per_item_shaped.len(), 2, "gen 0 never evicts");
14200
14201 c.per_item_accessed.insert(1);
14203 c.begin_generation();
14204 assert_eq!(c.per_item_shaped.len(), 1);
14205 assert!(c.per_item_shaped.contains_key(&1));
14206
14207 c.begin_generation();
14209 assert_eq!(
14210 c.per_item_shaped.len(),
14211 1,
14212 "an empty access-set must not wipe the cache"
14213 );
14214 }
14215
14216 #[test]
14217 fn use_old_layout_accepts_a_render_only_change_and_rejects_layout_changes() {
14218 let c = UnifiedConstraints::default();
14219 let red = styled(|s| {
14220 s.color = ColorU {
14221 r: 255,
14222 g: 0,
14223 b: 0,
14224 a: 255,
14225 };
14226 });
14227 let old = [text_content("hi", style())];
14228 let new_colour = [text_content("hi", red)];
14229 assert!(
14230 TextShapingCache::use_old_layout(&c, &c, &old, &new_colour),
14231 "a colour-only change must reuse the cached layout"
14232 );
14233
14234 let new_text = [text_content("ho", style())];
14236 assert!(!TextShapingCache::use_old_layout(&c, &c, &old, &new_text));
14237
14238 let bigger = [text_content("hi", styled(|s| s.font_size_px = 32.0))];
14240 assert!(!TextShapingCache::use_old_layout(&c, &c, &old, &bigger));
14241
14242 let c2 = UnifiedConstraints {
14244 available_width: AvailableSpace::Definite(100.0),
14245 ..Default::default()
14246 };
14247 assert!(!TextShapingCache::use_old_layout(&c, &c2, &old, &old));
14248 }
14249
14250 #[test]
14251 fn use_old_layout_on_empty_content_and_length_or_variant_mismatch() {
14252 let c = UnifiedConstraints::default();
14253 assert!(
14254 TextShapingCache::use_old_layout(&c, &c, &[], &[]),
14255 "empty vs empty is trivially reusable"
14256 );
14257 let one = [text_content("a", style())];
14258 assert!(!TextShapingCache::use_old_layout(&c, &c, &[], &one));
14259 assert!(!TextShapingCache::use_old_layout(&c, &c, &one, &[]));
14260
14261 let space = [InlineContent::Space(InlineSpace {
14263 width: 4.0,
14264 is_breaking: true,
14265 is_stretchy: true,
14266 })];
14267 assert!(!TextShapingCache::use_old_layout(&c, &c, &one, &space));
14268 assert!(TextShapingCache::use_old_layout(&c, &c, &space, &space));
14269 }
14270
14271 #[test]
14272 fn inline_content_layout_eq_recurses_into_ruby() {
14273 let ruby = |base: &str| InlineContent::Ruby {
14274 base: vec![text_content(base, style())],
14275 text: vec![text_content("ふり", style())],
14276 style: style(),
14277 };
14278 assert!(TextShapingCache::inline_content_layout_eq(
14279 &ruby("漢"),
14280 &ruby("漢")
14281 ));
14282 assert!(!TextShapingCache::inline_content_layout_eq(
14283 &ruby("漢"),
14284 &ruby("字")
14285 ));
14286 }
14287
14288 #[test]
14289 fn calculate_id_is_deterministic_and_discriminating() {
14290 assert_eq!(calculate_id(&"abc"), calculate_id(&"abc"));
14291 assert_ne!(calculate_id(&"abc"), calculate_id(&"abd"));
14292 assert_eq!(calculate_id(&0_u64), calculate_id(&0_u64));
14293 assert_ne!(calculate_id(&0_u64), calculate_id(&u64::MAX));
14294 let e: Vec<u8> = Vec::new();
14296 assert_eq!(calculate_id(&e), calculate_id(&Vec::<u8>::new()));
14297 }
14298
14299 #[test]
14300 fn shaped_items_key_new_on_empty_visual_items_is_stable() {
14301 let a = ShapedItemsKey::new(7, &[]);
14302 let b = ShapedItemsKey::new(7, &[]);
14303 assert_eq!(a, b);
14304 assert_eq!(hash_of(&a), hash_of(&b));
14305 assert_ne!(a, ShapedItemsKey::new(8, &[]));
14307 }
14308
14309 #[test]
14310 fn shaped_items_key_new_hashes_the_text_styles() {
14311 let vi = |st: Arc<StyleProperties>| VisualItem {
14312 logical_source: LogicalItem::Text {
14313 source: ci(0, 0),
14314 text: "a".to_string(),
14315 style: st,
14316 marker_position_outside: None,
14317 source_node_id: None,
14318 },
14319 bidi_level: BidiLevel::new(0),
14320 script: Script::Latin,
14321 text: "a".to_string(),
14322 run_byte_offset: 0,
14323 };
14324 let base = ShapedItemsKey::new(1, &[vi(style())]);
14325 let same = ShapedItemsKey::new(1, &[vi(style())]);
14326 let other = ShapedItemsKey::new(1, &[vi(styled(|s| s.font_size_px = 32.0))]);
14327 assert_eq!(base, same);
14328 assert_ne!(base.style_hash, other.style_hash, "font size must change the key");
14329 }
14330
14331 #[test]
14336 fn overflow_info_default_has_no_overflow() {
14337 let o = OverflowInfo::default();
14338 assert!(!o.has_overflow());
14339 assert_eq!(o.unclipped_bounds, Rect::default());
14340
14341 let with = OverflowInfo {
14342 overflow_items: vec![cl("a", 8.0)],
14343 unclipped_bounds: Rect::default(),
14344 };
14345 assert!(with.has_overflow());
14346 }
14347
14348 fn layout_of(items: Vec<PositionedItem>) -> UnifiedLayout {
14349 UnifiedLayout {
14350 items,
14351 overflow: OverflowInfo::default(),
14352 }
14353 }
14354
14355 #[test]
14356 fn unified_layout_empty_is_inert_across_every_accessor() {
14357 let l = layout_of(Vec::new());
14358 assert!(l.is_empty());
14359 assert_eq!(l.bounds(), Rect::default());
14360 assert_eq!(l.first_baseline(), None);
14361 assert_eq!(l.last_baseline(), None);
14362 assert_eq!(l.get_first_cluster_cursor(), None);
14363 assert_eq!(l.get_last_cluster_cursor(), None);
14364 assert!(l.grapheme_stops().is_empty());
14365 assert_eq!(
14366 l.hittest_cursor(LogicalPosition { x: 0.0, y: 0.0 }),
14367 None,
14368 "hit-testing an empty layout must return None, not index [0]"
14369 );
14370 assert_eq!(
14371 l.hittest_cursor(LogicalPosition {
14372 x: f32::NAN,
14373 y: f32::NAN
14374 }),
14375 None
14376 );
14377 }
14378
14379 #[test]
14380 fn unified_layout_bounds_spans_all_items() {
14381 let l = layout_of(vec![
14382 pos(cl("a", 10.0), 0.0, 0.0, 0),
14383 pos(cl("b", 10.0), 90.0, 20.0, 1),
14384 ]);
14385 let b = l.bounds();
14386 assert_eq!(b.x, 0.0);
14387 assert_eq!(b.y, 0.0);
14388 assert_eq!(b.width, 100.0, "0 → 90+10");
14389 approx(b.height, 36.0); assert!(!l.is_empty());
14391 }
14392
14393 #[test]
14394 fn unified_layout_baselines_skip_breaks_and_tabs() {
14395 let l = layout_of(vec![
14396 pos(brk(), 0.0, 0.0, 0),
14397 pos(cl("a", 10.0), 0.0, 0.0, 0),
14398 pos(obj(10.0, 20.0, 5.0), 10.0, 0.0, 0),
14399 pos(tab(8.0, 16.0), 20.0, 0.0, 0),
14400 ]);
14401 approx(
14402 l.first_baseline().expect("the cluster, not the break"),
14403 12.8,
14404 );
14405 assert_eq!(l.last_baseline(), Some(5.0), "the object, not the tab");
14406 }
14407
14408 #[test]
14409 fn unified_layout_cluster_cursors_skip_non_clusters() {
14410 let l = layout_of(vec![
14411 pos(brk(), 0.0, 0.0, 0),
14412 pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0),
14413 pos(cl_at("b", 10.0, 0, 1), 10.0, 0.0, 0),
14414 pos(tab(8.0, 16.0), 20.0, 0.0, 0),
14415 ]);
14416 assert_eq!(
14417 l.get_first_cluster_cursor(),
14418 Some(TextCursor {
14419 cluster_id: gid(0, 0),
14420 affinity: CursorAffinity::Leading
14421 })
14422 );
14423 assert_eq!(
14424 l.get_last_cluster_cursor(),
14425 Some(TextCursor {
14426 cluster_id: gid(0, 1),
14427 affinity: CursorAffinity::Trailing
14428 })
14429 );
14430
14431 let no_clusters = layout_of(vec![pos(brk(), 0.0, 0.0, 0)]);
14433 assert_eq!(no_clusters.get_first_cluster_cursor(), None);
14434 assert_eq!(no_clusters.get_last_cluster_cursor(), None);
14435 }
14436
14437 #[test]
14438 fn unified_layout_grapheme_stops_sorts_dedups_and_folds_combining_marks() {
14439 let l = layout_of(vec![
14441 pos(cl_at("b", 10.0, 0, 1), 10.0, 0.0, 0),
14442 pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0),
14443 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), ]);
14446 let stops = l.grapheme_stops();
14447 assert_eq!(
14448 stops,
14449 vec![gid(0, 0), gid(0, 1)],
14450 "sorted, de-duplicated, with the combining mark folded away"
14451 );
14452 }
14453
14454 #[test]
14455 fn unified_layout_cluster_is_grapheme_continuation() {
14456 assert!(UnifiedLayout::cluster_is_grapheme_continuation("\u{0301}"));
14457 assert!(UnifiedLayout::cluster_is_grapheme_continuation("\u{FE0F}"), "VS-16");
14458 assert!(!UnifiedLayout::cluster_is_grapheme_continuation("a"));
14459 assert!(!UnifiedLayout::cluster_is_grapheme_continuation("中"));
14460 assert!(
14461 !UnifiedLayout::cluster_is_grapheme_continuation(""),
14462 "an empty cluster must return false, not panic"
14463 );
14464 }
14465
14466 #[test]
14467 fn unified_layout_grapheme_caret_offset_maps_affinity_and_gaps() {
14468 let stops = [gid(0, 0), gid(0, 1), gid(0, 2)];
14469 assert_eq!(
14470 UnifiedLayout::grapheme_caret_offset(
14471 &stops,
14472 &TextCursor {
14473 cluster_id: gid(0, 0),
14474 affinity: CursorAffinity::Leading
14475 }
14476 ),
14477 Some(0)
14478 );
14479 assert_eq!(
14480 UnifiedLayout::grapheme_caret_offset(
14481 &stops,
14482 &TextCursor {
14483 cluster_id: gid(0, 2),
14484 affinity: CursorAffinity::Trailing
14485 }
14486 ),
14487 Some(3),
14488 "the document end is len, i.e. one past the last stop"
14489 );
14490 assert_eq!(
14492 UnifiedLayout::grapheme_caret_offset(
14493 &stops,
14494 &TextCursor {
14495 cluster_id: gid(0, 99),
14496 affinity: CursorAffinity::Leading
14497 }
14498 ),
14499 Some(2)
14500 );
14501 assert_eq!(
14503 UnifiedLayout::grapheme_caret_offset(
14504 &[gid(5, 5)],
14505 &TextCursor {
14506 cluster_id: gid(0, 0),
14507 affinity: CursorAffinity::Leading
14508 }
14509 ),
14510 None
14511 );
14512 assert_eq!(
14514 UnifiedLayout::grapheme_caret_offset(
14515 &[],
14516 &TextCursor {
14517 cluster_id: gid(0, 0),
14518 affinity: CursorAffinity::Leading
14519 }
14520 ),
14521 None
14522 );
14523 }
14524
14525 #[test]
14526 fn unified_layout_cursor_from_grapheme_offset_clamps_past_the_end() {
14527 let stops = [gid(0, 0), gid(0, 1)];
14528 assert_eq!(
14529 UnifiedLayout::cursor_from_grapheme_offset(&stops, 0),
14530 TextCursor {
14531 cluster_id: gid(0, 0),
14532 affinity: CursorAffinity::Leading
14533 }
14534 );
14535 assert_eq!(
14536 UnifiedLayout::cursor_from_grapheme_offset(&stops, 1),
14537 TextCursor {
14538 cluster_id: gid(0, 1),
14539 affinity: CursorAffinity::Leading
14540 }
14541 );
14542 let end = TextCursor {
14544 cluster_id: gid(0, 1),
14545 affinity: CursorAffinity::Trailing,
14546 };
14547 assert_eq!(UnifiedLayout::cursor_from_grapheme_offset(&stops, 2), end);
14548 assert_eq!(
14549 UnifiedLayout::cursor_from_grapheme_offset(&stops, usize::MAX),
14550 end,
14551 "usize::MAX must clamp, not overflow"
14552 );
14553 }
14554
14555 #[test]
14556 #[should_panic]
14557 fn unified_layout_cursor_from_grapheme_offset_panics_on_an_empty_stop_list() {
14558 let _ = UnifiedLayout::cursor_from_grapheme_offset(&[], 0);
14563 }
14564
14565 #[test]
14566 fn unified_layout_cursor_motion_on_an_empty_layout_returns_the_cursor_unchanged() {
14567 let l = layout_of(Vec::new());
14568 let c = TextCursor {
14569 cluster_id: gid(0, 0),
14570 affinity: CursorAffinity::Leading,
14571 };
14572 let mut dbg = None;
14573 assert_eq!(l.move_cursor_left(c, &mut dbg), c);
14574 assert_eq!(l.move_cursor_right(c, &mut dbg), c);
14575 assert_eq!(l.move_cursor_to_line_start(c, &mut dbg), c);
14576 assert_eq!(l.move_cursor_to_line_end(c, &mut dbg), c);
14577 assert_eq!(l.move_cursor_to_prev_word(c, &mut dbg), c);
14578 assert_eq!(l.move_cursor_to_next_word(c, &mut dbg), c);
14579 let mut goal = None;
14580 assert_eq!(l.move_cursor_up(c, &mut goal, &mut dbg), c);
14581 assert_eq!(l.move_cursor_down(c, &mut goal, &mut dbg), c);
14582 }
14583
14584 #[test]
14585 fn unified_layout_move_cursor_left_right_walk_one_grapheme_at_a_time() {
14586 let l = layout_of(vec![
14587 pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0),
14588 pos(cl_at("b", 10.0, 0, 1), 10.0, 0.0, 0),
14589 pos(cl_at("c", 10.0, 0, 2), 20.0, 0.0, 0),
14590 ]);
14591 let mut dbg = None;
14592 let start = TextCursor {
14593 cluster_id: gid(0, 0),
14594 affinity: CursorAffinity::Leading,
14595 };
14596
14597 assert_eq!(l.move_cursor_left(start, &mut dbg), start);
14599
14600 let c1 = l.move_cursor_right(start, &mut dbg);
14602 assert_eq!(c1.cluster_id, gid(0, 1));
14603 let c2 = l.move_cursor_right(c1, &mut dbg);
14604 assert_eq!(c2.cluster_id, gid(0, 2));
14605 let end = l.move_cursor_right(c2, &mut dbg);
14606 assert_eq!(end.cluster_id, gid(0, 2));
14607 assert_eq!(end.affinity, CursorAffinity::Trailing, "document end");
14608 assert_eq!(l.move_cursor_right(end, &mut dbg), end);
14610
14611 assert_eq!(l.move_cursor_left(end, &mut dbg).cluster_id, gid(0, 2));
14613 }
14614
14615 #[test]
14616 fn unified_layout_hittest_cursor_picks_the_nearest_cluster_and_its_half() {
14617 let l = layout_of(vec![
14618 pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0),
14619 pos(cl_at("b", 10.0, 0, 1), 10.0, 0.0, 0),
14620 ]);
14621 let hit = |x: f32| l.hittest_cursor(LogicalPosition { x, y: 5.0 }).unwrap();
14622 assert_eq!(hit(1.0).cluster_id, gid(0, 0));
14623 assert_eq!(hit(1.0).affinity, CursorAffinity::Leading);
14624 assert_eq!(hit(9.0).affinity, CursorAffinity::Trailing, "right half of 'a'");
14625 assert_eq!(hit(11.0).cluster_id, gid(0, 1));
14626 assert_eq!(hit(-1000.0).cluster_id, gid(0, 0));
14628 assert_eq!(hit(1000.0).cluster_id, gid(0, 1));
14629 }
14630
14631 #[test]
14632 fn unified_layout_get_selection_rects_on_an_unknown_range_is_empty() {
14633 let l = layout_of(vec![pos(cl_at("a", 10.0, 0, 0), 0.0, 0.0, 0)]);
14634 let unknown = SelectionRange {
14635 start: TextCursor {
14636 cluster_id: gid(9, 9),
14637 affinity: CursorAffinity::Leading,
14638 },
14639 end: TextCursor {
14640 cluster_id: gid(9, 9),
14641 affinity: CursorAffinity::Trailing,
14642 },
14643 };
14644 assert!(l.get_selection_rects(&unknown).is_empty());
14645
14646 let collapsed = SelectionRange {
14648 start: TextCursor {
14649 cluster_id: gid(0, 0),
14650 affinity: CursorAffinity::Leading,
14651 },
14652 end: TextCursor {
14653 cluster_id: gid(0, 0),
14654 affinity: CursorAffinity::Leading,
14655 },
14656 };
14657 let _ = l.get_selection_rects(&collapsed);
14658 }
14659
14660 #[test]
14661 fn unified_layout_get_cursor_rect_for_known_and_unknown_cursors() {
14662 let l = layout_of(vec![pos(cl_at("a", 10.0, 0, 0), 5.0, 7.0, 0)]);
14663 let leading = l.get_cursor_rect(&TextCursor {
14664 cluster_id: gid(0, 0),
14665 affinity: CursorAffinity::Leading,
14666 });
14667 let r = leading.expect("the leading edge of a placed cluster must have a rect");
14668 assert_eq!(r.origin.x, 5.0);
14669 assert_eq!(r.origin.y, 7.0);
14670 assert_eq!(r.size.width, 1.0, "the caret is a 1px sliver");
14671
14672 assert_eq!(
14674 l.get_cursor_rect(&TextCursor {
14675 cluster_id: gid(9, 0),
14676 affinity: CursorAffinity::Leading
14677 }),
14678 None
14679 );
14680 assert_eq!(
14682 layout_of(Vec::new()).get_cursor_rect(&TextCursor {
14683 cluster_id: gid(0, 0),
14684 affinity: CursorAffinity::Leading
14685 }),
14686 None
14687 );
14688 }
14689
14690 #[test]
14695 fn break_cursor_new_on_an_empty_slice_is_both_at_start_and_done() {
14696 let items: Vec<ShapedItem> = Vec::new();
14697 let mut c = BreakCursor::new(&items);
14698 assert!(c.is_at_start());
14699 assert!(c.is_done());
14700 assert_eq!(c.word_break, WordBreak::Normal);
14701 assert_eq!(c.hyphens, Hyphens::default());
14702 assert_eq!(c.line_break, LineBreakStrictness::default());
14703 assert!(c.peek_next_unit().is_empty());
14704 assert!(c.peek_next_single_item().is_empty());
14705 assert!(c.drain_remaining().is_empty());
14706 }
14707
14708 #[test]
14709 fn break_cursor_with_word_break_stores_the_mode() {
14710 let items = vec![cl("a", 8.0)];
14711 let c = BreakCursor::with_word_break(&items, WordBreak::BreakAll);
14712 assert_eq!(c.word_break, WordBreak::BreakAll);
14713 assert!(c.is_at_start());
14714 assert!(!c.is_done());
14715 }
14716
14717 #[test]
14718 fn break_cursor_consume_zero_is_a_no_op() {
14719 let items = vec![cl("a", 8.0), cl("b", 8.0)];
14720 let mut c = BreakCursor::new(&items);
14721 c.consume(0);
14722 assert!(c.is_at_start());
14723 assert_eq!(c.next_item_index, 0);
14724 }
14725
14726 #[test]
14727 fn break_cursor_consume_advances_and_ends_the_stream() {
14728 let items = vec![cl("a", 8.0), cl("b", 8.0)];
14729 let mut c = BreakCursor::new(&items);
14730 c.consume(1);
14731 assert!(!c.is_at_start());
14732 assert!(!c.is_done());
14733 assert_eq!(c.peek_next_single_item().len(), 1);
14734 c.consume(1);
14735 assert!(c.is_done());
14736 assert!(c.peek_next_single_item().is_empty());
14737 }
14738
14739 #[test]
14740 fn break_cursor_over_consuming_past_the_end_still_reports_done() {
14741 let items = vec![cl("a", 8.0)];
14742 let mut c = BreakCursor::new(&items);
14743 c.consume(usize::MAX);
14744 assert!(c.is_done(), "an over-consume must not wrap around to not-done");
14745 assert!(
14746 c.drain_remaining().is_empty(),
14747 "drain_remaining is bounds-guarded"
14748 );
14749 }
14750
14751 #[test]
14752 #[should_panic]
14753 fn break_cursor_peek_next_unit_after_over_consuming_slices_out_of_bounds() {
14754 let items = vec![cl("a", 8.0)];
14759 let mut c = BreakCursor::new(&items);
14760 c.consume(5);
14761 let _ = c.peek_next_unit();
14762 }
14763
14764 #[test]
14765 fn break_cursor_drains_the_remainder_before_the_main_list() {
14766 let items = vec![cl("a", 8.0), cl("b", 8.0)];
14767 let mut c = BreakCursor::new(&items);
14768 c.partial_remainder = vec![cl("R", 8.0)];
14769 assert!(!c.is_at_start(), "a pending remainder means we are mid-stream");
14770 assert!(!c.is_done());
14771
14772 assert_eq!(
14773 c.peek_next_single_item()[0].as_cluster().unwrap().text,
14774 "R",
14775 "the remainder is served first"
14776 );
14777
14778 let drained = c.drain_remaining();
14779 assert_eq!(drained.len(), 3, "remainder + both queued items");
14780 assert_eq!(drained[0].as_cluster().unwrap().text, "R");
14781 assert!(c.is_done());
14782 }
14783
14784 #[test]
14785 fn break_cursor_is_done_is_false_while_a_remainder_is_pending() {
14786 let items: Vec<ShapedItem> = Vec::new();
14787 let mut c = BreakCursor::new(&items);
14788 c.partial_remainder = vec![cl("x", 8.0)];
14789 assert!(!c.is_done(), "the main list is exhausted but the remainder is not");
14790 c.consume(1);
14791 assert!(c.is_done());
14792 }
14793
14794 #[test]
14795 fn break_cursor_consume_spanning_remainder_and_main_list() {
14796 let items = vec![cl("a", 8.0), cl("b", 8.0), cl("c", 8.0)];
14797 let mut c = BreakCursor::new(&items);
14798 c.partial_remainder = vec![cl("R1", 8.0), cl("R2", 8.0)];
14799 c.consume(3);
14801 assert!(c.partial_remainder.is_empty());
14802 assert_eq!(c.next_item_index, 1);
14803 assert_eq!(c.peek_next_single_item()[0].as_cluster().unwrap().text, "b");
14804 }
14805
14806 #[test]
14807 fn break_cursor_peek_next_unit_returns_a_whole_word_then_the_space() {
14808 let items = vec![
14809 cl("h", 8.0),
14810 cl("i", 8.0),
14811 cl(" ", 4.0),
14812 cl("y", 8.0),
14813 cl("o", 8.0),
14814 ];
14815 let mut c = BreakCursor::new(&items);
14816 let word = c.peek_next_unit();
14817 assert_eq!(word.len(), 2, "the word stops at the space");
14818 assert_eq!(word[0].as_cluster().unwrap().text, "h");
14819
14820 c.consume(word.len());
14821 let space = c.peek_next_unit();
14822 assert_eq!(space.len(), 1, "a leading break opportunity is a unit on its own");
14823 assert_eq!(space[0].as_cluster().unwrap().text, " ");
14824
14825 c.consume(1);
14826 assert_eq!(c.peek_next_unit().len(), 2, "the trailing word");
14827 }
14828
14829 #[test]
14830 fn break_cursor_peek_next_unit_honours_break_all_and_keep_all() {
14831 let items = vec![cl("中", 16.0), cl("文", 16.0), cl("字", 16.0)];
14832
14833 let normal = BreakCursor::new(&items);
14834 assert_eq!(
14835 normal.peek_next_unit().len(),
14836 1,
14837 "word-break:normal — each ideograph is its own unit"
14838 );
14839
14840 let all = BreakCursor::with_word_break(&items, WordBreak::BreakAll);
14841 assert_eq!(all.peek_next_unit().len(), 1, "break-all — one cluster per unit");
14842
14843 let keep = BreakCursor::with_word_break(&items, WordBreak::KeepAll);
14844 assert_eq!(
14845 keep.peek_next_unit().len(),
14846 3,
14847 "keep-all — the whole CJK run is unbreakable"
14848 );
14849
14850 let latin = vec![cl("a", 8.0), cl("b", 8.0)];
14851 let latin_all = BreakCursor::with_word_break(&latin, WordBreak::BreakAll);
14852 assert_eq!(latin_all.peek_next_unit().len(), 1, "break-all splits Latin too");
14853 }
14854
14855 #[test]
14856 fn break_cursor_peek_next_unit_glues_across_a_word_joiner() {
14857 let plain = vec![cl("a", 8.0), cl(" ", 4.0), cl("b", 8.0)];
14859 let control = BreakCursor::new(&plain);
14860 assert_eq!(
14861 control.peek_next_unit().len(),
14862 1,
14863 "the unit normally stops before the space"
14864 );
14865
14866 let glued = vec![
14869 cl("a", 8.0),
14870 cl("\u{2060}", 0.0),
14871 cl(" ", 4.0),
14872 cl("b", 8.0),
14873 ];
14874 let c = BreakCursor::new(&glued);
14875 let unit = c.peek_next_unit();
14876 assert!(
14877 unit.len() > 1,
14878 "a word joiner must suppress the following break, got {} item(s)",
14879 unit.len()
14880 );
14881 }
14882
14883 #[test]
14884 fn break_cursor_peek_next_single_item_prefers_the_remainder() {
14885 let items = vec![cl("a", 8.0)];
14886 let mut c = BreakCursor::new(&items);
14887 assert_eq!(c.peek_next_single_item()[0].as_cluster().unwrap().text, "a");
14888 c.partial_remainder = vec![cl("R", 8.0)];
14889 assert_eq!(c.peek_next_single_item()[0].as_cluster().unwrap().text, "R");
14890 assert_eq!(
14891 c.peek_next_single_item().len(),
14892 1,
14893 "peek must never return more than one item"
14894 );
14895 }
14896
14897 fn tf(hash: u64) -> TestFont {
14902 TestFont { hash }
14903 }
14904
14905 #[test]
14906 fn loaded_fonts_new_is_empty_and_misses_every_lookup() {
14907 let lf: LoadedFonts<TestFont> = LoadedFonts::new();
14908 assert!(lf.is_empty());
14909 assert_eq!(lf.len(), 0);
14910 assert_eq!(lf.iter().count(), 0);
14911 assert!(lf.get(&FontId(0)).is_none());
14912 assert!(!lf.contains_key(&FontId(u128::MAX)));
14913 for h in [0_u64, 1, u64::MAX] {
14915 assert!(lf.get_by_hash(h).is_none());
14916 assert!(lf.get_font_id_by_hash(h).is_none());
14917 assert!(!lf.contains_hash(h));
14918 }
14919 let d: LoadedFonts<TestFont> = LoadedFonts::default();
14921 assert!(d.is_empty());
14922 }
14923
14924 #[test]
14925 fn loaded_fonts_insert_indexes_by_id_and_by_hash() {
14926 let mut lf: LoadedFonts<TestFont> = LoadedFonts::new();
14927 lf.insert(FontId(1), tf(0xDEAD));
14928 assert_eq!(lf.len(), 1);
14929 assert!(!lf.is_empty());
14930 assert!(lf.contains_key(&FontId(1)));
14931 assert!(lf.contains_hash(0xDEAD));
14932 assert_eq!(lf.get(&FontId(1)).map(TestFont::get_hash), Some(0xDEAD));
14933 assert_eq!(lf.get_by_hash(0xDEAD).map(TestFont::get_hash), Some(0xDEAD));
14934 assert_eq!(lf.get_font_id_by_hash(0xDEAD), Some(&FontId(1)));
14935 assert!(lf.get_by_hash(0).is_none());
14936 }
14937
14938 #[test]
14939 fn loaded_fonts_zero_hash_is_a_valid_key_not_a_sentinel() {
14940 let mut lf: LoadedFonts<TestFont> = LoadedFonts::new();
14941 lf.insert(FontId(1), tf(0));
14942 assert!(lf.contains_hash(0), "hash 0 must be storable and findable");
14943 assert_eq!(lf.get_by_hash(0).map(TestFont::get_hash), Some(0));
14944 }
14945
14946 #[test]
14947 fn loaded_fonts_two_ids_sharing_a_hash_keep_only_the_last_reverse_mapping() {
14948 let mut lf: LoadedFonts<TestFont> = LoadedFonts::new();
14949 lf.insert(FontId(1), tf(7));
14950 lf.insert(FontId(2), tf(7));
14951 assert_eq!(lf.len(), 2, "both fonts are stored by id");
14952 assert_eq!(
14953 lf.get_font_id_by_hash(7),
14954 Some(&FontId(2)),
14955 "the reverse index keeps only the LAST id for a colliding hash"
14956 );
14957 }
14958
14959 #[test]
14960 fn loaded_fonts_replacing_a_font_id_leaves_a_stale_hash_mapping() {
14961 let mut lf: LoadedFonts<TestFont> = LoadedFonts::new();
14966 lf.insert(FontId(1), tf(100));
14967 lf.insert(FontId(1), tf(200)); assert_eq!(lf.len(), 1, "the font map correctly holds one entry");
14969
14970 assert!(
14971 lf.contains_hash(100),
14972 "the old hash is still in the reverse index"
14973 );
14974 let stale = lf.get_by_hash(100).expect("stale mapping resolves");
14975 assert_eq!(
14976 stale.get_hash(),
14977 200,
14978 "looking up the OLD hash hands back the NEW font"
14979 );
14980 }
14981
14982 #[test]
14983 fn loaded_fonts_from_iterator_matches_repeated_inserts() {
14984 let lf: LoadedFonts<TestFont> =
14985 vec![(FontId(1), tf(10)), (FontId(2), tf(20))].into_iter().collect();
14986 assert_eq!(lf.len(), 2);
14987 assert!(lf.contains_hash(10) && lf.contains_hash(20));
14988 assert_eq!(lf.iter().count(), 2);
14989 }
14990
14991 fn manager() -> FontManager<TestFont> {
14996 FontManager::new(FcFontCache::default()).expect("FontManager::new must not fail")
14997 }
14998
14999 #[test]
15000 fn font_manager_constructors_start_empty() {
15001 for m in [
15002 manager(),
15003 FontManager::from_shared(FcFontCache::default()).unwrap(),
15004 FontManager::from_arc_shared(
15005 FcFontCache::default(),
15006 Arc::new(Mutex::new(HashMap::new())),
15007 )
15008 .unwrap(),
15009 ] {
15010 assert!(m.get_font_chain_cache().is_empty());
15011 assert!(m.get_loaded_fonts().is_empty());
15012 assert!(m.get_loaded_font_ids().is_empty());
15013 assert!(m.registry.is_none());
15014 assert_eq!(m.last_resolved_font_stacks_sig, None);
15015 assert!(m.get_font_by_hash(0).is_none());
15016 assert!(m.get_embedded_font_by_hash(u64::MAX).is_none());
15017 }
15018 }
15019
15020 #[test]
15021 fn font_manager_from_arc_shared_sees_writes_through_the_shared_pool() {
15022 let pool: Arc<Mutex<HashMap<FontId, TestFont>>> = Arc::new(Mutex::new(HashMap::new()));
15023 let a = FontManager::from_arc_shared(FcFontCache::default(), pool.clone()).unwrap();
15024 let b = FontManager::from_arc_shared(FcFontCache::default(), pool).unwrap();
15025
15026 assert!(a.insert_font(FontId(1), tf(5)).is_none(), "no previous font");
15027 assert_eq!(
15028 b.get_loaded_fonts().len(),
15029 1,
15030 "the second manager must observe the first's insert"
15031 );
15032 assert_eq!(b.get_font_by_hash(5).map(|f| f.get_hash()), Some(5));
15033
15034 assert!(Arc::ptr_eq(&a.shared_parsed_fonts(), &b.shared_parsed_fonts()));
15036 }
15037
15038 #[test]
15039 fn font_manager_insert_font_returns_the_replaced_font() {
15040 let m = manager();
15041 assert!(m.insert_font(FontId(1), tf(1)).is_none());
15042 let old = m.insert_font(FontId(1), tf(2)).expect("must return the old font");
15043 assert_eq!(old.get_hash(), 1);
15044 assert_eq!(m.get_loaded_fonts().len(), 1);
15045 }
15046
15047 #[test]
15048 fn font_manager_insert_fonts_and_remove_font() {
15049 let m = manager();
15050 m.insert_fonts(vec![(FontId(1), tf(1)), (FontId(2), tf(2))]);
15051 assert_eq!(m.get_loaded_font_ids().len(), 2);
15052
15053 assert_eq!(m.remove_font(&FontId(1)).map(|f| f.get_hash()), Some(1));
15054 assert!(m.remove_font(&FontId(1)).is_none(), "double-remove is a no-op");
15055 assert!(
15056 m.remove_font(&FontId(u128::MAX)).is_none(),
15057 "removing an unknown id must not panic"
15058 );
15059 assert_eq!(m.get_loaded_fonts().len(), 1);
15060
15061 m.insert_fonts(Vec::new());
15063 assert_eq!(m.get_loaded_fonts().len(), 1);
15064 }
15065
15066 #[test]
15067 fn font_manager_get_font_by_hash_scans_linearly_and_misses_cleanly() {
15068 let m = manager();
15069 m.insert_fonts(vec![(FontId(1), tf(11)), (FontId(2), tf(22))]);
15070 assert_eq!(m.get_font_by_hash(22).map(|f| f.get_hash()), Some(22));
15071 assert!(m.get_font_by_hash(33).is_none());
15072 assert!(m.get_font_by_hash(u64::MAX).is_none());
15073 assert!(m.get_font_by_hash(0).is_none());
15074 }
15075
15076 #[test]
15077 fn font_manager_chain_cache_set_merge_and_signature() {
15078 let mut m = manager();
15079 assert!(m.get_font_chain_cache().is_empty());
15080
15081 m.set_font_chain_cache_with_sig(HashMap::new(), Some(42));
15083 assert_eq!(m.last_resolved_font_stacks_sig, Some(42));
15084
15085 m.set_font_chain_cache(HashMap::new());
15087 assert_eq!(
15088 m.last_resolved_font_stacks_sig, None,
15089 "a signature-less set must invalidate the recorded signature"
15090 );
15091
15092 m.merge_font_chain_cache(HashMap::new());
15094 assert!(m.get_font_chain_cache().is_empty());
15095 }
15096
15097 #[test]
15098 fn font_manager_garbage_collect_evicts_everything_not_in_the_keep_set() {
15099 let mut m = manager();
15100 m.insert_fonts(vec![
15101 (FontId(1), tf(1)),
15102 (FontId(2), tf(2)),
15103 (FontId(3), tf(3)),
15104 ]);
15105
15106 let mut keep = HashSet::new();
15107 keep.insert(FontId(2));
15108 let evicted = m.garbage_collect_fonts(&keep, &HashSet::new());
15109 assert_eq!(evicted, 2);
15110 assert_eq!(m.get_loaded_font_ids(), keep);
15111
15112 assert_eq!(m.garbage_collect_fonts(&keep, &HashSet::new()), 0);
15114
15115 assert_eq!(m.garbage_collect_fonts(&HashSet::new(), &HashSet::new()), 1);
15117 assert!(m.get_loaded_fonts().is_empty());
15118 assert_eq!(m.garbage_collect_fonts(&HashSet::new(), &HashSet::new()), 0);
15120 }
15121
15122 #[test]
15123 fn font_manager_load_missing_for_chains_with_no_chains_loads_nothing() {
15124 use crate::solver3::getters::ResolvedFontChains;
15125 let m = manager();
15126 let empty = ResolvedFontChains {
15127 chains: HashMap::new(),
15128 ..Default::default()
15129 };
15130 let failed = m.load_missing_for_chains(
15131 &empty,
15132 |_bytes, _idx| -> Result<TestFont, LayoutError> {
15133 panic!("the loader must never be invoked when there is nothing to load")
15134 },
15135 );
15136 assert!(failed.is_empty());
15137 assert!(m.get_loaded_fonts().is_empty());
15138 }
15139
15140 #[test]
15141 fn font_context_from_fc_cache_starts_empty_and_converts_to_a_manager() {
15142 let ctx = FontContext::from_fc_cache(FcFontCache::default());
15143 assert!(ctx.font_chain_cache.is_empty());
15144 assert!(ctx.embedded_fonts.is_empty());
15145 assert!(ctx.font_hash_to_families.is_empty());
15146 assert!(ctx.registry.is_none());
15147 assert!(ctx.parsed_fonts.lock().unwrap().is_empty());
15148
15149 ctx.load_fonts_for_chains();
15151 assert!(ctx.parsed_fonts.lock().unwrap().is_empty());
15152
15153 let mgr = ctx.to_font_manager();
15154 assert!(mgr.get_font_chain_cache().is_empty());
15155 assert!(mgr.registry.is_none());
15156 assert_eq!(mgr.last_resolved_font_stacks_sig, None);
15157 assert!(
15158 Arc::ptr_eq(&mgr.parsed_fonts, &ctx.parsed_fonts),
15159 "the manager must share (not copy) the parsed-font pool"
15160 );
15161 }
15162
15163 #[test]
15168 fn create_logical_items_on_empty_and_whitespace_only_content() {
15169 let mut dbg = None;
15170 assert!(create_logical_items(&[], &[], &mut dbg).is_empty());
15171
15172 let empty_run = [text_content("", style())];
15174 assert!(create_logical_items(&empty_run, &[], &mut dbg).is_empty());
15175
15176 let ws = [text_content(" \t\n", style())];
15178 assert!(!create_logical_items(&ws, &[], &mut dbg).is_empty());
15179 }
15180
15181 #[test]
15182 fn create_logical_items_handles_multibyte_and_astral_text_without_panicking() {
15183 let mut dbg = None;
15184 for s in ["\u{1F600}", "é\u{0301}", "中文", "a\u{200B}b", "\u{FEFF}"] {
15185 let content = [text_content(s, style())];
15186 let items = create_logical_items(&content, &[], &mut dbg);
15187 assert!(!items.is_empty(), "{s:?} produced no logical items");
15188 }
15189 }
15190
15191 #[test]
15192 fn create_logical_items_on_a_long_run_does_not_hang() {
15193 let mut dbg = None;
15194 let long = "a".repeat(100_000);
15195 let content = [text_content(&long, style())];
15196 let items = create_logical_items(&content, &[], &mut dbg);
15197 assert!(!items.is_empty());
15198 }
15199
15200 #[test]
15201 fn create_logical_items_debug_messages_are_recorded_when_requested() {
15202 let mut dbg = Some(Vec::new());
15203 let content = [text_content("hi", style())];
15204 let _ = create_logical_items(&content, &[], &mut dbg);
15205 assert!(
15206 !dbg.expect("Some(..) in → Some(..) out").is_empty(),
15207 "the debug sink must be populated when it is Some"
15208 );
15209 }
15210
15211 #[test]
15212 fn get_base_direction_from_logical_defaults_to_ltr_on_empty_input() {
15213 assert_eq!(get_base_direction_from_logical(&[]), BidiDirection::Ltr);
15214
15215 let mut dbg = None;
15216 let ltr = create_logical_items(&[text_content("hello", style())], &[], &mut dbg);
15217 assert_eq!(get_base_direction_from_logical(<r), BidiDirection::Ltr);
15218
15219 let rtl = create_logical_items(&[text_content("\u{05D0}\u{05D1}", style())], &[], &mut dbg);
15221 assert_eq!(get_base_direction_from_logical(&rtl), BidiDirection::Rtl);
15222 }
15223
15224 #[test]
15225 fn reorder_logical_items_on_empty_input_is_ok_and_empty() {
15226 let mut dbg = None;
15227 let out = reorder_logical_items(&[], BidiDirection::Ltr, UnicodeBidi::Normal, &mut dbg)
15228 .expect("reordering nothing must succeed");
15229 assert!(out.is_empty());
15230 }
15231
15232 #[test]
15233 fn reorder_logical_items_preserves_content_for_pure_ltr_text() {
15234 let mut dbg = None;
15235 let logical = create_logical_items(&[text_content("abc", style())], &[], &mut dbg);
15236 let visual = reorder_logical_items(
15237 &logical,
15238 BidiDirection::Ltr,
15239 UnicodeBidi::Normal,
15240 &mut dbg,
15241 )
15242 .expect("LTR reordering must succeed");
15243 let joined: String = visual.iter().map(|v| v.text.as_str()).collect();
15244 assert_eq!(joined, "abc", "pure LTR text must survive bidi unchanged");
15245 assert!(visual.iter().all(|v| !v.bidi_level.is_rtl()));
15246 }
15247
15248 #[cfg(not(feature = "text_layout_hyphenation"))]
15253 #[test]
15254 fn stub_hyphenate_never_reports_a_break() {
15255 let s = Standard;
15256 assert!(s.hyphenate("").breaks.is_empty());
15257 assert!(s.hyphenate("hyphenation").breaks.is_empty());
15258 assert!(s.hyphenate(&"a".repeat(10_000)).breaks.is_empty());
15259 assert!(s.hyphenate("\u{1F600}\u{0301}").breaks.is_empty());
15260 }
15261
15262 #[cfg(not(feature = "text_layout_hyphenation"))]
15263 #[test]
15264 fn stub_get_hyphenator_always_errors() {
15265 assert!(matches!(
15266 get_hyphenator(Language::EnglishUS),
15267 Err(LayoutError::HyphenationError(_))
15268 ));
15269 }
15270
15271 #[cfg(feature = "text_layout_hyphenation")]
15272 #[test]
15273 fn get_hyphenator_loads_an_embedded_language_and_hyphenates() {
15274 let h =
15275 get_hyphenator(HyphenationLanguage::EnglishUS).expect("en-US dictionaries are embedded");
15276
15277 let empty = h.hyphenate("");
15279 assert!(empty.breaks.is_empty(), "an empty word must not panic");
15280 let one = h.hyphenate("a");
15281 assert!(one.breaks.is_empty());
15282
15283 let word = "hyphenation";
15285 let opps = h.hyphenate(word);
15286 for &b in &opps.breaks {
15287 assert!(b > 0 && b < word.len(), "break {b} is outside {word:?}");
15288 assert!(word.is_char_boundary(b), "break {b} splits a char");
15289 }
15290
15291 let weird = h.hyphenate("\u{1F600}\u{0301}");
15293 assert!(weird.breaks.len() < 8, "no runaway break list");
15294 }
15295}