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, FcWeight, FontId, PatternMatch, UnicodeRange};
44use smallvec::{smallvec, SmallVec};
45use unicode_bidi::{BidiInfo, Level, TextSource};
46use unicode_segmentation::UnicodeSegmentation;
47
48pub type ShapedGlyphVec = SmallVec<[ShapedGlyph; 1]>;
54
55#[derive(Debug, Clone, Copy)]
62pub enum LineHeight {
63 Normal,
65 Px(f32),
67}
68
69impl Default for LineHeight {
70 fn default() -> Self {
71 LineHeight::Normal
72 }
73}
74
75impl LineHeight {
76 pub fn resolve(&self, font_size_px: f32, ascent: f32, descent: f32, line_gap: f32, units_per_em: u16) -> f32 {
81 match self {
82 LineHeight::Px(px) => *px,
83 LineHeight::Normal => {
84 if units_per_em == 0 {
85 return font_size_px * 1.2; }
87 let scale = font_size_px / units_per_em as f32;
88 (ascent - descent + line_gap) * scale
89 }
90 }
91 }
92
93 pub fn resolve_with_metrics(&self, font_size_px: f32, metrics: &LayoutFontMetrics) -> f32 {
95 self.resolve(font_size_px, metrics.ascent, metrics.descent, metrics.line_gap, metrics.units_per_em)
96 }
97}
98
99impl PartialEq for LineHeight {
100 fn eq(&self, other: &Self) -> bool {
101 match (self, other) {
102 (LineHeight::Normal, LineHeight::Normal) => true,
103 (LineHeight::Px(a), LineHeight::Px(b)) => a.to_bits() == b.to_bits(),
104 _ => false,
105 }
106 }
107}
108
109impl Eq for LineHeight {}
110
111impl Hash for LineHeight {
112 fn hash<H: Hasher>(&self, state: &mut H) {
113 std::mem::discriminant(self).hash(state);
114 if let LineHeight::Px(v) = self {
115 v.to_bits().hash(state);
116 }
117 }
118}
119
120#[cfg(not(feature = "text_layout_hyphenation"))]
122pub struct Standard;
123
124#[cfg(not(feature = "text_layout_hyphenation"))]
125impl Standard {
126 pub fn hyphenate<'a>(&'a self, _word: &'a str) -> StubHyphenationBreaks {
128 StubHyphenationBreaks { breaks: Vec::new() }
129 }
130}
131
132#[cfg(not(feature = "text_layout_hyphenation"))]
134pub struct StubHyphenationBreaks {
135 pub breaks: Vec<usize>,
136}
137
138use crate::text3::script::{script_to_language, Language, Script};
140
141#[derive(Debug, Clone, Copy, PartialEq)]
152pub enum AvailableSpace {
153 Definite(f32),
157 MinContent,
159 MaxContent,
162}
163
164impl Default for AvailableSpace {
165 fn default() -> Self {
168 AvailableSpace::MaxContent
169 }
170}
171
172impl AvailableSpace {
173 pub fn is_definite(&self) -> bool {
175 matches!(self, AvailableSpace::Definite(_))
176 }
177
178 pub fn is_indefinite(&self) -> bool {
180 !self.is_definite()
181 }
182
183 pub fn unwrap_or(self, fallback: f32) -> f32 {
185 match self {
186 AvailableSpace::Definite(v) => v,
187 _ => fallback,
188 }
189 }
190
191 pub fn to_f32_for_layout(self) -> f32 {
197 match self {
198 AvailableSpace::Definite(v) => v,
199 AvailableSpace::MinContent => f32::MAX / 2.0,
200 AvailableSpace::MaxContent => f32::MAX / 2.0,
201 }
202 }
203
204 pub fn from_f32(value: f32) -> Self {
214 if value.is_infinite() || value >= f32::MAX / 2.0 {
215 AvailableSpace::MaxContent
217 } else if value <= 0.0 {
218 AvailableSpace::MinContent
220 } else {
221 AvailableSpace::Definite(value)
222 }
223 }
224}
225
226impl Hash for AvailableSpace {
227 fn hash<H: Hasher>(&self, state: &mut H) {
228 std::mem::discriminant(self).hash(state);
229 if let AvailableSpace::Definite(v) = self {
230 (v.round() as usize).hash(state);
231 }
232 }
233}
234
235pub use crate::font_traits::{ParsedFontTrait, ShallowClone};
237
238#[derive(Debug, Clone, PartialEq, Eq, Hash)]
242pub struct FontChainKey {
243 pub font_families: Vec<String>,
244 pub weight: FcWeight,
245 pub italic: bool,
246 pub oblique: bool,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Hash)]
255pub enum FontChainKeyOrRef {
256 Chain(FontChainKey),
258 Ref(usize),
260}
261
262impl FontChainKeyOrRef {
263 pub fn from_font_stack(font_stack: &FontStack) -> Self {
265 match font_stack {
266 FontStack::Stack(selectors) => FontChainKeyOrRef::Chain(FontChainKey::from_selectors(selectors)),
267 FontStack::Ref(font_ref) => FontChainKeyOrRef::Ref(font_ref.parsed as usize),
268 }
269 }
270
271 pub fn is_ref(&self) -> bool {
273 matches!(self, FontChainKeyOrRef::Ref(_))
274 }
275
276 pub fn as_ref_ptr(&self) -> Option<usize> {
278 match self {
279 FontChainKeyOrRef::Ref(ptr) => Some(*ptr),
280 _ => None,
281 }
282 }
283
284 pub fn as_chain(&self) -> Option<&FontChainKey> {
286 match self {
287 FontChainKeyOrRef::Chain(key) => Some(key),
288 _ => None,
289 }
290 }
291}
292
293impl FontChainKey {
294 pub fn from_selectors(font_stack: &[FontSelector]) -> Self {
296 let font_families: Vec<String> = font_stack
297 .iter()
298 .map(|s| s.family.clone())
299 .filter(|f| !f.is_empty())
300 .collect();
301
302 let font_families = if font_families.is_empty() {
303 vec!["serif".to_string()]
304 } else {
305 font_families
306 };
307
308 let weight = font_stack
309 .first()
310 .map(|s| s.weight)
311 .unwrap_or(FcWeight::Normal);
312 let is_italic = font_stack
313 .first()
314 .map(|s| s.style == FontStyle::Italic)
315 .unwrap_or(false);
316 let is_oblique = font_stack
317 .first()
318 .map(|s| s.style == FontStyle::Oblique)
319 .unwrap_or(false);
320
321 FontChainKey {
322 font_families,
323 weight,
324 italic: is_italic,
325 oblique: is_oblique,
326 }
327 }
328}
329
330#[derive(Debug, Clone)]
337pub struct LoadedFonts<T> {
338 pub fonts: HashMap<FontId, T>,
340 hash_to_id: HashMap<u64, FontId>,
342}
343
344impl<T: ParsedFontTrait> LoadedFonts<T> {
345 pub fn new() -> Self {
346 Self {
347 fonts: HashMap::new(),
348 hash_to_id: HashMap::new(),
349 }
350 }
351
352 pub fn insert(&mut self, font_id: FontId, font: T) {
354 let hash = font.get_hash();
355 self.hash_to_id.insert(hash, font_id.clone());
356 self.fonts.insert(font_id, font);
357 }
358
359 pub fn get(&self, font_id: &FontId) -> Option<&T> {
361 self.fonts.get(font_id)
362 }
363
364 pub fn get_by_hash(&self, hash: u64) -> Option<&T> {
366 self.hash_to_id.get(&hash).and_then(|id| self.fonts.get(id))
367 }
368
369 pub fn get_font_id_by_hash(&self, hash: u64) -> Option<&FontId> {
371 self.hash_to_id.get(&hash)
372 }
373
374 pub fn contains_key(&self, font_id: &FontId) -> bool {
376 self.fonts.contains_key(font_id)
377 }
378
379 pub fn contains_hash(&self, hash: u64) -> bool {
381 self.hash_to_id.contains_key(&hash)
382 }
383
384 pub fn iter(&self) -> impl Iterator<Item = (&FontId, &T)> {
386 self.fonts.iter()
387 }
388
389 pub fn len(&self) -> usize {
391 self.fonts.len()
392 }
393
394 pub fn is_empty(&self) -> bool {
396 self.fonts.is_empty()
397 }
398}
399
400impl<T: ParsedFontTrait> Default for LoadedFonts<T> {
401 fn default() -> Self {
402 Self::new()
403 }
404}
405
406impl<T: ParsedFontTrait> FromIterator<(FontId, T)> for LoadedFonts<T> {
407 fn from_iter<I: IntoIterator<Item = (FontId, T)>>(iter: I) -> Self {
408 let mut loaded = LoadedFonts::new();
409 for (id, font) in iter {
410 loaded.insert(id, font);
411 }
412 loaded
413 }
414}
415
416#[derive(Debug, Clone)]
421pub enum FontOrRef<T> {
422 Font(T),
424 Ref(azul_css::props::basic::FontRef),
426}
427
428impl<T: ParsedFontTrait> ShallowClone for FontOrRef<T> {
429 fn shallow_clone(&self) -> Self {
430 match self {
431 FontOrRef::Font(f) => FontOrRef::Font(f.shallow_clone()),
432 FontOrRef::Ref(r) => FontOrRef::Ref(r.clone()),
433 }
434 }
435}
436
437impl<T: ParsedFontTrait> ParsedFontTrait for FontOrRef<T> {
438 fn shape_text(
439 &self,
440 text: &str,
441 script: Script,
442 language: Language,
443 direction: BidiDirection,
444 style: &StyleProperties,
445 ) -> Result<Vec<Glyph>, LayoutError> {
446 match self {
447 FontOrRef::Font(f) => f.shape_text(text, script, language, direction, style),
448 FontOrRef::Ref(r) => r.shape_text(text, script, language, direction, style),
449 }
450 }
451
452 fn get_hash(&self) -> u64 {
453 match self {
454 FontOrRef::Font(f) => f.get_hash(),
455 FontOrRef::Ref(r) => r.get_hash(),
456 }
457 }
458
459 fn get_glyph_size(&self, glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
460 match self {
461 FontOrRef::Font(f) => f.get_glyph_size(glyph_id, font_size),
462 FontOrRef::Ref(r) => r.get_glyph_size(glyph_id, font_size),
463 }
464 }
465
466 fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
467 match self {
468 FontOrRef::Font(f) => f.get_hyphen_glyph_and_advance(font_size),
469 FontOrRef::Ref(r) => r.get_hyphen_glyph_and_advance(font_size),
470 }
471 }
472
473 fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
474 match self {
475 FontOrRef::Font(f) => f.get_kashida_glyph_and_advance(font_size),
476 FontOrRef::Ref(r) => r.get_kashida_glyph_and_advance(font_size),
477 }
478 }
479
480 fn has_glyph(&self, codepoint: u32) -> bool {
481 match self {
482 FontOrRef::Font(f) => f.has_glyph(codepoint),
483 FontOrRef::Ref(r) => r.has_glyph(codepoint),
484 }
485 }
486
487 fn get_vertical_metrics(&self, glyph_id: u16) -> Option<VerticalMetrics> {
488 match self {
489 FontOrRef::Font(f) => f.get_vertical_metrics(glyph_id),
490 FontOrRef::Ref(r) => r.get_vertical_metrics(glyph_id),
491 }
492 }
493
494 fn get_font_metrics(&self) -> LayoutFontMetrics {
495 match self {
496 FontOrRef::Font(f) => f.get_font_metrics(),
497 FontOrRef::Ref(r) => r.get_font_metrics(),
498 }
499 }
500
501 fn num_glyphs(&self) -> u16 {
502 match self {
503 FontOrRef::Font(f) => f.num_glyphs(),
504 FontOrRef::Ref(r) => r.num_glyphs(),
505 }
506 }
507
508 fn get_space_width(&self) -> Option<usize> {
509 match self {
510 FontOrRef::Font(f) => f.get_space_width(),
511 FontOrRef::Ref(r) => r.get_space_width(),
512 }
513 }
514}
515
516#[derive(Debug, Clone)]
533pub struct FontContext {
534 pub fc_cache: FcFontCache,
539 pub parsed_fonts: Arc<Mutex<HashMap<FontId, azul_css::props::basic::FontRef>>>,
540 pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
541 pub embedded_fonts: HashMap<u64, azul_css::props::basic::FontRef>,
542 pub font_hash_to_families: HashMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
545 pub registry: Option<Arc<rust_fontconfig::registry::FcFontRegistry>>,
551}
552
553impl FontContext {
554 pub fn from_fc_cache(fc_cache: FcFontCache) -> Self {
563 Self {
564 fc_cache,
565 parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
566 font_chain_cache: HashMap::new(),
567 embedded_fonts: HashMap::new(),
568 font_hash_to_families: HashMap::new(),
569 registry: None,
570 }
571 }
572
573 pub fn from_registry(
585 registry: Arc<rust_fontconfig::registry::FcFontRegistry>,
586 ) -> Self {
587 let fc_cache = registry.shared_cache();
588 Self {
589 fc_cache,
590 parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
591 font_chain_cache: HashMap::new(),
592 embedded_fonts: HashMap::new(),
593 font_hash_to_families: HashMap::new(),
594 registry: Some(registry),
595 }
596 }
597
598 pub fn pre_resolve_chains_for_dom(
607 &mut self,
608 styled_dom: &azul_core::styled_dom::StyledDom,
609 platform: &azul_css::system::Platform,
610 ) {
611 use crate::solver3::getters::{
612 collect_font_stacks_from_styled_dom, collect_used_codepoints,
613 prune_chain_to_used_chars, resolve_font_chains, scripts_present_in_styled_dom,
614 };
615 let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
616 let scripts = scripts_present_in_styled_dom(styled_dom);
617 let mut chains = resolve_font_chains(&collected, &self.fc_cache, Some(&scripts));
618 let used_chars = collect_used_codepoints(styled_dom);
620 for chain in chains.chains.values_mut() {
621 prune_chain_to_used_chars(chain, &used_chars);
622 }
623 self.font_chain_cache = chains.into_fontconfig_chains();
624 }
625
626 pub fn load_fonts_for_chains(&self) {
636 use crate::solver3::getters::ResolvedFontChains;
637 use crate::text3::default::PathLoader;
638
639 let chains_map: HashMap<FontChainKeyOrRef, _> = self
640 .font_chain_cache
641 .iter()
642 .map(|(k, v)| (FontChainKeyOrRef::Chain(k.clone()), v.clone()))
643 .collect();
644 let resolved = ResolvedFontChains { chains: chains_map };
645
646 let manager = match FontManager::<azul_css::props::basic::FontRef>::from_arc_shared(
650 self.fc_cache.clone(),
651 self.parsed_fonts.clone(),
652 ) {
653 Ok(m) => m,
654 Err(_) => return,
655 };
656 let loader = PathLoader::new();
657 let _failed = manager
658 .load_missing_for_chains(&resolved, |bytes, idx| loader.load_font_shared(bytes, idx));
659 }
660
661 pub fn to_font_manager(&self) -> FontManager<azul_css::props::basic::FontRef> {
665 FontManager {
666 fc_cache: self.fc_cache.clone(),
667 parsed_fonts: self.parsed_fonts.clone(),
668 font_chain_cache: self.font_chain_cache.clone(),
669 embedded_fonts: Mutex::new(self.embedded_fonts.clone()),
670 font_hash_to_families: self.font_hash_to_families.clone(),
671 registry: self.registry.clone(),
672 last_resolved_font_stacks_sig: None,
673 }
674 }
675}
676
677#[derive(Debug)]
678pub struct FontManager<T> {
679 pub fc_cache: FcFontCache,
684 pub parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
688 pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
691 pub embedded_fonts: Mutex<HashMap<u64, azul_css::props::basic::FontRef>>,
694 pub font_hash_to_families: HashMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
698 pub registry: Option<Arc<rust_fontconfig::registry::FcFontRegistry>>,
705 pub last_resolved_font_stacks_sig: Option<u64>,
712}
713
714impl<T: ParsedFontTrait> FontManager<T> {
715 pub fn new(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
716 Ok(Self {
717 fc_cache,
718 parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
719 font_chain_cache: HashMap::new(),
720 embedded_fonts: Mutex::new(HashMap::new()),
721 font_hash_to_families: HashMap::new(),
722 registry: None,
723 last_resolved_font_stacks_sig: None,
724 })
725 }
726
727 pub fn from_shared(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
734 Ok(Self {
735 fc_cache,
736 parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
737 font_chain_cache: HashMap::new(),
738 embedded_fonts: Mutex::new(HashMap::new()),
739 font_hash_to_families: HashMap::new(),
740 registry: None,
741 last_resolved_font_stacks_sig: None,
742 })
743 }
744
745 pub fn from_arc_shared(
751 fc_cache: FcFontCache,
752 parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
753 ) -> Result<Self, LayoutError> {
754 Ok(Self {
755 fc_cache,
756 parsed_fonts,
757 font_chain_cache: HashMap::new(),
758 embedded_fonts: Mutex::new(HashMap::new()),
759 font_hash_to_families: HashMap::new(),
760 registry: None,
761 last_resolved_font_stacks_sig: None,
762 })
763 }
764
765 pub fn with_registry(
769 mut self,
770 registry: Arc<rust_fontconfig::registry::FcFontRegistry>,
771 ) -> Self {
772 self.registry = Some(registry);
773 self
774 }
775
776 pub fn shared_parsed_fonts(&self) -> Arc<Mutex<HashMap<FontId, T>>> {
781 Arc::clone(&self.parsed_fonts)
782 }
783
784 pub fn set_font_chain_cache(
789 &mut self,
790 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
791 ) {
792 self.font_chain_cache = chains;
793 self.last_resolved_font_stacks_sig = None;
794 }
795
796 pub fn set_font_chain_cache_with_sig(
802 &mut self,
803 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
804 sig: Option<u64>,
805 ) {
806 self.font_chain_cache = chains;
807 self.last_resolved_font_stacks_sig = sig;
808 }
809
810 pub fn merge_font_chain_cache(
814 &mut self,
815 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
816 ) {
817 self.font_chain_cache.extend(chains);
818 }
819
820 pub fn get_font_chain_cache(
822 &self,
823 ) -> &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain> {
824 &self.font_chain_cache
825 }
826
827 pub fn get_embedded_font_by_hash(&self, font_hash: u64) -> Option<azul_css::props::basic::FontRef> {
830 let embedded = self.embedded_fonts.lock().unwrap();
831 embedded.get(&font_hash).cloned()
832 }
833
834 pub fn get_font_by_hash(&self, font_hash: u64) -> Option<T> {
837 let parsed = self.parsed_fonts.lock().unwrap();
838 for (_, font) in parsed.iter() {
840 if font.get_hash() == font_hash {
841 return Some(font.clone());
842 }
843 }
844 None
845 }
846
847 pub fn register_embedded_font(&self, font_ref: &azul_css::props::basic::FontRef) {
850 let hash = font_ref.get_hash();
851 let mut embedded = self.embedded_fonts.lock().unwrap();
852 embedded.insert(hash, font_ref.clone());
853 }
854
855 pub fn get_loaded_fonts(&self) -> LoadedFonts<T> {
862 let parsed = self.parsed_fonts.lock().unwrap();
863 parsed
864 .iter()
865 .map(|(id, font)| (id.clone(), font.shallow_clone()))
866 .collect()
867 }
868
869 pub fn get_loaded_font_ids(&self) -> std::collections::HashSet<FontId> {
874 let parsed = self.parsed_fonts.lock().unwrap();
875 if parsed.is_empty() {
880 return std::collections::HashSet::new();
881 }
882 parsed.keys().cloned().collect()
883 }
884
885 pub fn insert_font(&self, font_id: FontId, font: T) -> Option<T> {
889 let mut parsed = self.parsed_fonts.lock().unwrap();
890 parsed.insert(font_id, font)
891 }
892
893 pub fn insert_fonts(&self, fonts: impl IntoIterator<Item = (FontId, T)>) {
898 let mut parsed = self.parsed_fonts.lock().unwrap();
899 for (font_id, font) in fonts {
900 parsed.insert(font_id, font);
901 }
902 }
903
904 pub fn load_missing_for_chains<F>(
916 &self,
917 chains: &crate::solver3::getters::ResolvedFontChains,
918 load_fn: F,
919 ) -> Vec<(FontId, String)>
920 where
921 F: Fn(std::sync::Arc<rust_fontconfig::FontBytes>, usize) -> Result<T, LayoutError>,
922 {
923 use crate::solver3::getters::{
924 collect_font_ids_from_chains, compute_fonts_to_load, load_fonts_from_disk,
925 };
926 let required = collect_font_ids_from_chains(chains);
927 let already = self.get_loaded_font_ids();
928 let to_load = compute_fonts_to_load(&required, &already);
929 if to_load.is_empty() {
930 return Vec::new();
931 }
932 let result = load_fonts_from_disk(&to_load, &self.fc_cache, load_fn);
933 self.insert_fonts(result.loaded);
934 result.failed
935 }
936
937 pub fn remove_font(&self, font_id: &FontId) -> Option<T> {
941 let mut parsed = self.parsed_fonts.lock().unwrap();
942 parsed.remove(font_id)
943 }
944}
945
946#[derive(Debug, thiserror::Error)]
948pub enum LayoutError {
949 #[error("Bidi analysis failed: {0}")]
950 BidiError(String),
951 #[error("Shaping failed: {0}")]
952 ShapingError(String),
953 #[error("Font not found: {0:?}")]
954 FontNotFound(FontSelector),
955 #[error("Invalid text input: {0}")]
956 InvalidText(String),
957 #[error("Hyphenation failed: {0}")]
958 HyphenationError(String),
959}
960
961#[derive(Debug, Clone, Copy, PartialEq, Eq)]
963pub enum TextBoundary {
964 Top,
966 Bottom,
968 Start,
970 End,
972}
973
974#[derive(Debug, Clone, Copy, PartialEq, Eq)]
976pub struct CursorBoundsError {
977 pub boundary: TextBoundary,
979 pub cursor: TextCursor,
981}
982
983#[derive(Debug, Clone)]
1054pub struct UnifiedConstraints {
1055 pub shape_boundaries: Vec<ShapeBoundary>,
1057 pub shape_exclusions: Vec<ShapeBoundary>,
1058
1059 pub available_width: AvailableSpace,
1061 pub available_height: Option<f32>,
1062
1063 pub writing_mode: Option<WritingMode>,
1065 pub direction: Option<BidiDirection>,
1068 pub text_orientation: TextOrientation,
1069 pub text_align: TextAlign,
1070 pub text_justify: JustifyContent,
1071 pub line_height: LineHeight,
1073 pub vertical_align: VerticalAlign,
1074 pub strut_ascent: f32,
1076 pub strut_descent: f32,
1077 pub strut_x_height: f32,
1079
1080 pub ch_width: f32,
1083
1084 pub overflow: OverflowBehavior,
1086 pub segment_alignment: SegmentAlignment,
1087
1088 pub text_combine_upright: Option<TextCombineUpright>,
1090 pub exclusion_margin: f32,
1091 pub hyphenation: Hyphens,
1092 pub hyphenation_language: Option<Language>,
1093 pub text_indent: f32,
1094 pub text_indent_each_line: bool,
1095 pub text_indent_hanging: bool,
1096 pub initial_letter: Option<InitialLetter>,
1097 pub line_clamp: Option<NonZeroUsize>,
1098
1099 pub text_wrap: TextWrap,
1101 pub columns: u32,
1102 pub column_gap: f32,
1103 pub hanging_punctuation: bool,
1104 pub overflow_wrap: OverflowWrap,
1105 pub text_align_last: TextAlign,
1106 pub word_break: WordBreak,
1108 pub white_space_mode: WhiteSpaceMode,
1109 pub line_break: LineBreakStrictness,
1110 pub unicode_bidi: UnicodeBidi,
1112}
1113
1114impl Default for UnifiedConstraints {
1115 fn default() -> Self {
1116 Self {
1117 shape_boundaries: Vec::new(),
1118 shape_exclusions: Vec::new(),
1119
1120 available_width: AvailableSpace::MaxContent,
1127 available_height: None,
1128 writing_mode: None,
1129 direction: None, text_orientation: TextOrientation::default(),
1131 text_align: TextAlign::default(),
1132 text_justify: JustifyContent::default(),
1133 line_height: LineHeight::Normal,
1134 vertical_align: VerticalAlign::default(),
1135 strut_ascent: 12.8, strut_descent: 3.2, strut_x_height: 8.0, ch_width: 8.0, overflow: OverflowBehavior::default(),
1140 segment_alignment: SegmentAlignment::default(),
1141 text_combine_upright: None,
1142 exclusion_margin: 0.0,
1143 hyphenation: Hyphens::default(),
1144 hyphenation_language: None,
1145 columns: 1,
1146 column_gap: 0.0,
1147 hanging_punctuation: false,
1148 text_indent: 0.0,
1149 text_indent_each_line: false,
1150 text_indent_hanging: false,
1151 initial_letter: None,
1152 line_clamp: None,
1153 text_wrap: TextWrap::default(),
1154 overflow_wrap: OverflowWrap::default(),
1155 text_align_last: TextAlign::default(),
1156 word_break: WordBreak::default(),
1157 white_space_mode: WhiteSpaceMode::default(),
1158 line_break: LineBreakStrictness::default(),
1159 unicode_bidi: UnicodeBidi::default(),
1160 }
1161 }
1162}
1163
1164impl Hash for UnifiedConstraints {
1166 fn hash<H: Hasher>(&self, state: &mut H) {
1167 self.shape_boundaries.hash(state);
1168 self.shape_exclusions.hash(state);
1169 self.available_width.hash(state);
1170 self.available_height
1171 .map(|h| h.round() as usize)
1172 .hash(state);
1173 self.writing_mode.hash(state);
1174 self.direction.hash(state);
1175 self.text_orientation.hash(state);
1176 self.text_align.hash(state);
1177 self.text_justify.hash(state);
1178 self.line_height.hash(state);
1179 self.vertical_align.hash(state);
1180 (self.strut_ascent.round() as usize).hash(state);
1181 (self.strut_descent.round() as usize).hash(state);
1182 (self.strut_x_height.round() as usize).hash(state);
1183 (self.ch_width.round() as usize).hash(state);
1184 self.overflow.hash(state);
1185 self.segment_alignment.hash(state);
1186 self.text_combine_upright.hash(state);
1187 (self.exclusion_margin.round() as usize).hash(state);
1188 self.hyphenation.hash(state);
1189 self.hyphenation_language.hash(state);
1190 (self.text_indent.round() as usize).hash(state);
1191 self.text_indent_each_line.hash(state);
1192 self.text_indent_hanging.hash(state);
1193 self.initial_letter.hash(state);
1194 self.line_clamp.hash(state);
1195 self.columns.hash(state);
1196 (self.column_gap.round() as usize).hash(state);
1197 self.hanging_punctuation.hash(state);
1198 self.overflow_wrap.hash(state);
1199 self.text_align_last.hash(state);
1200 self.word_break.hash(state);
1201 self.white_space_mode.hash(state);
1202 self.line_break.hash(state);
1203 self.unicode_bidi.hash(state);
1204 }
1205}
1206
1207impl PartialEq for UnifiedConstraints {
1208 fn eq(&self, other: &Self) -> bool {
1209 self.shape_boundaries == other.shape_boundaries
1210 && self.shape_exclusions == other.shape_exclusions
1211 && self.available_width == other.available_width
1212 && match (self.available_height, other.available_height) {
1213 (None, None) => true,
1214 (Some(h1), Some(h2)) => round_eq(h1, h2),
1215 _ => false,
1216 }
1217 && self.writing_mode == other.writing_mode
1218 && self.direction == other.direction
1219 && self.text_orientation == other.text_orientation
1220 && self.text_align == other.text_align
1221 && self.text_justify == other.text_justify
1222 && self.line_height == other.line_height
1223 && self.vertical_align == other.vertical_align
1224 && round_eq(self.strut_ascent, other.strut_ascent)
1225 && round_eq(self.strut_descent, other.strut_descent)
1226 && round_eq(self.strut_x_height, other.strut_x_height)
1227 && round_eq(self.ch_width, other.ch_width)
1228 && self.overflow == other.overflow
1229 && self.segment_alignment == other.segment_alignment
1230 && self.text_combine_upright == other.text_combine_upright
1231 && round_eq(self.exclusion_margin, other.exclusion_margin)
1232 && self.hyphenation == other.hyphenation
1233 && self.hyphenation_language == other.hyphenation_language
1234 && round_eq(self.text_indent, other.text_indent)
1235 && self.text_indent_each_line == other.text_indent_each_line
1236 && self.text_indent_hanging == other.text_indent_hanging
1237 && self.initial_letter == other.initial_letter
1238 && self.line_clamp == other.line_clamp
1239 && self.columns == other.columns
1240 && round_eq(self.column_gap, other.column_gap)
1241 && self.hanging_punctuation == other.hanging_punctuation
1242 && self.overflow_wrap == other.overflow_wrap
1243 && self.text_align_last == other.text_align_last
1244 && self.word_break == other.word_break
1245 && self.white_space_mode == other.white_space_mode
1246 && self.line_break == other.line_break
1247 && self.unicode_bidi == other.unicode_bidi
1248 }
1249}
1250
1251impl Eq for UnifiedConstraints {}
1252
1253impl UnifiedConstraints {
1254 pub fn resolved_line_height(&self) -> f32 {
1257 let font_size_approx = self.strut_ascent + self.strut_descent;
1258 self.line_height.resolve(font_size_approx, 0.0, 0.0, 0.0, 0)
1259 }
1260 fn direction(&self, fallback: BidiDirection) -> BidiDirection {
1261 match self.writing_mode {
1262 Some(s) => s.get_direction().unwrap_or(fallback),
1263 None => fallback,
1264 }
1265 }
1266 fn is_vertical(&self) -> bool {
1267 matches!(
1268 self.writing_mode,
1269 Some(WritingMode::VerticalRl) | Some(WritingMode::VerticalLr)
1270 )
1271 }
1272}
1273
1274#[derive(Debug, Clone)]
1276pub struct LineConstraints {
1277 pub segments: Vec<LineSegment>,
1278 pub total_available: f32,
1279}
1280
1281impl WritingMode {
1282 fn get_direction(&self) -> Option<BidiDirection> {
1283 match self {
1284 WritingMode::HorizontalTb => None,
1286 WritingMode::VerticalRl => Some(BidiDirection::Rtl),
1287 WritingMode::VerticalLr => Some(BidiDirection::Ltr),
1288 WritingMode::SidewaysRl => Some(BidiDirection::Rtl),
1289 WritingMode::SidewaysLr => Some(BidiDirection::Ltr),
1290 }
1291 }
1292}
1293
1294#[derive(Debug, Clone, Hash)]
1296pub struct StyledRun {
1297 pub text: String,
1298 pub style: Arc<StyleProperties>,
1299 pub logical_start_byte: usize,
1301 pub source_node_id: Option<NodeId>,
1304}
1305
1306#[derive(Debug, Clone)]
1308pub struct VisualRun<'a> {
1309 pub text_slice: &'a str,
1310 pub style: Arc<StyleProperties>,
1311 pub logical_start_byte: usize,
1312 pub bidi_level: BidiLevel,
1313 pub script: Script,
1314 pub language: Language,
1315}
1316
1317#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1322pub struct FontSelector {
1323 pub family: String,
1324 pub weight: FcWeight,
1325 pub style: FontStyle,
1326 pub unicode_ranges: Vec<UnicodeRange>,
1327}
1328
1329impl Default for FontSelector {
1330 fn default() -> Self {
1331 Self {
1332 family: "serif".to_string(),
1333 weight: FcWeight::Normal,
1334 style: FontStyle::Normal,
1335 unicode_ranges: Vec::new(),
1336 }
1337 }
1338}
1339
1340#[derive(Debug, Clone)]
1347pub enum FontStack {
1348 Stack(Vec<FontSelector>),
1351 Ref(azul_css::props::basic::font::FontRef),
1354}
1355
1356impl Default for FontStack {
1357 fn default() -> Self {
1358 FontStack::Stack(vec![FontSelector::default()])
1359 }
1360}
1361
1362impl FontStack {
1363 pub fn is_ref(&self) -> bool {
1365 matches!(self, FontStack::Ref(_))
1366 }
1367
1368 pub fn as_ref(&self) -> Option<&azul_css::props::basic::font::FontRef> {
1370 match self {
1371 FontStack::Ref(r) => Some(r),
1372 _ => None,
1373 }
1374 }
1375
1376 pub fn as_stack(&self) -> Option<&[FontSelector]> {
1378 match self {
1379 FontStack::Stack(s) => Some(s),
1380 _ => None,
1381 }
1382 }
1383
1384 pub fn first_selector(&self) -> Option<&FontSelector> {
1386 match self {
1387 FontStack::Stack(s) => s.first(),
1388 FontStack::Ref(_) => None,
1389 }
1390 }
1391
1392 pub fn first_family(&self) -> &str {
1394 match self {
1395 FontStack::Stack(s) => s.first().map(|f| f.family.as_str()).unwrap_or("serif"),
1396 FontStack::Ref(_) => "<embedded-font>",
1397 }
1398 }
1399}
1400
1401impl PartialEq for FontStack {
1402 fn eq(&self, other: &Self) -> bool {
1403 match (self, other) {
1404 (FontStack::Stack(a), FontStack::Stack(b)) => a == b,
1405 (FontStack::Ref(a), FontStack::Ref(b)) => a.parsed == b.parsed,
1406 _ => false,
1407 }
1408 }
1409}
1410
1411impl Eq for FontStack {}
1412
1413impl Hash for FontStack {
1414 fn hash<H: Hasher>(&self, state: &mut H) {
1415 core::mem::discriminant(self).hash(state);
1416 match self {
1417 FontStack::Stack(s) => s.hash(state),
1418 FontStack::Ref(r) => (r.parsed as usize).hash(state),
1419 }
1420 }
1421}
1422
1423#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1427pub struct FontHash {
1428 pub font_hash: u64,
1430}
1431
1432impl FontHash {
1433 pub fn invalid() -> Self {
1434 Self { font_hash: 0 }
1435 }
1436
1437 pub fn from_hash(font_hash: u64) -> Self {
1438 Self { font_hash }
1439 }
1440}
1441
1442#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1443pub enum FontStyle {
1444 Normal,
1445 Italic,
1446 Oblique,
1447}
1448
1449#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1451pub enum SegmentAlignment {
1452 #[default]
1454 First,
1455 Total,
1458}
1459
1460#[derive(Debug, Clone)]
1461pub struct VerticalMetrics {
1462 pub advance: f32,
1463 pub bearing_x: f32,
1464 pub bearing_y: f32,
1465 pub origin_y: f32,
1466}
1467
1468#[derive(Debug, Clone)]
1480pub struct LayoutFontMetrics {
1481 pub ascent: f32,
1482 pub descent: f32,
1483 pub line_gap: f32,
1484 pub units_per_em: u16,
1485 pub x_height: Option<f32>,
1488 pub cap_height: Option<f32>,
1491}
1492
1493impl LayoutFontMetrics {
1494 pub fn baseline_scaled(&self, font_size: f32) -> f32 {
1498 let scale = font_size / self.units_per_em as f32;
1499 self.ascent * scale
1500 }
1501
1502 pub fn x_height_scaled(&self, font_size: f32) -> f32 {
1505 let scale = font_size / self.units_per_em as f32;
1506 match self.x_height {
1507 Some(xh) => xh * scale,
1508 None => font_size * 0.5,
1509 }
1510 }
1511
1512 pub fn cap_height_scaled(&self, font_size: f32) -> f32 {
1515 let scale = font_size / self.units_per_em as f32;
1516 self.cap_height.unwrap_or(self.ascent) * scale
1517 }
1518
1519 pub fn from_font_metrics(metrics: &azul_css::props::basic::FontMetrics) -> Self {
1539 let ascent = metrics.s_typo_ascender
1540 .as_option()
1541 .map(|v| *v as f32)
1542 .unwrap_or(metrics.ascender as f32);
1543 let descent = metrics.s_typo_descender
1544 .as_option()
1545 .map(|v| *v as f32)
1546 .unwrap_or(metrics.descender as f32);
1547 let line_gap = metrics.s_typo_line_gap
1550 .as_option()
1551 .map(|v| *v as f32)
1552 .unwrap_or(metrics.line_gap as f32)
1553 .max(0.0);
1554 let x_height = metrics.sx_height
1555 .as_option()
1556 .map(|v| *v as f32);
1557 let cap_height = metrics.s_cap_height
1558 .as_option()
1559 .map(|v| *v as f32);
1560 Self {
1561 ascent,
1562 descent,
1563 line_gap,
1564 units_per_em: metrics.units_per_em,
1565 x_height,
1566 cap_height,
1567 }
1568 }
1569
1570 pub fn em_over(&self) -> f32 {
1575 let central = self.central_baseline();
1576 central + (self.units_per_em as f32 / 2.0)
1577 }
1578
1579 pub fn em_under(&self) -> f32 {
1582 let central = self.central_baseline();
1583 central - (self.units_per_em as f32 / 2.0)
1584 }
1585
1586 pub fn central_baseline(&self) -> f32 {
1589 (self.ascent + self.descent) / 2.0
1590 }
1591}
1592
1593#[derive(Debug, Clone)]
1594pub struct LineSegment {
1595 pub start_x: f32,
1596 pub width: f32,
1597 pub priority: u8,
1599}
1600
1601#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
1602pub enum TextWrap {
1603 #[default]
1604 Wrap,
1605 Balance,
1606 NoWrap,
1607}
1608
1609#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1614pub enum OverflowWrap {
1615 #[default]
1617 Normal,
1618 Anywhere,
1622 BreakWord,
1626}
1627
1628#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1632pub enum Hyphens {
1633 None,
1635 #[default]
1637 Manual,
1638 Auto,
1640}
1641
1642#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
1646pub enum WhiteSpaceMode {
1647 #[default]
1648 Normal,
1649 Nowrap,
1650 Pre,
1651 PreWrap,
1652 PreLine,
1653 BreakSpaces,
1654}
1655
1656#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
1663pub enum LineBreakStrictness {
1664 #[default]
1665 Auto,
1666 Loose,
1667 Normal,
1668 Strict,
1669 Anywhere,
1672}
1673
1674#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
1676pub enum WordBreak {
1677 #[default]
1680 Normal,
1681 BreakAll,
1683 KeepAll,
1686}
1687
1688#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
1696pub struct InitialLetter {
1697 pub size: f32,
1699 pub sink: u32,
1702 pub count: NonZeroUsize,
1704 pub align: InitialLetterAlign,
1708}
1709
1710#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1713pub enum InitialLetterAlign {
1714 Auto,
1716 Alphabetic,
1718 Hanging,
1720 Ideographic,
1722}
1723
1724impl Eq for InitialLetter {}
1729
1730impl Hash for InitialLetter {
1731 fn hash<H: Hasher>(&self, state: &mut H) {
1732 (self.size.round() as usize).hash(state);
1737 self.sink.hash(state);
1738 self.count.hash(state);
1739 self.align.hash(state);
1740 }
1741}
1742
1743#[derive(Debug, Clone, PartialOrd)]
1745pub enum PathSegment {
1746 MoveTo(Point),
1747 LineTo(Point),
1748 CurveTo {
1749 control1: Point,
1750 control2: Point,
1751 end: Point,
1752 },
1753 QuadTo {
1754 control: Point,
1755 end: Point,
1756 },
1757 Arc {
1758 center: Point,
1759 radius: f32,
1760 start_angle: f32,
1761 end_angle: f32,
1762 },
1763 Close,
1764}
1765
1766impl Hash for PathSegment {
1768 fn hash<H: Hasher>(&self, state: &mut H) {
1769 discriminant(self).hash(state);
1771
1772 match self {
1773 PathSegment::MoveTo(p) => p.hash(state),
1774 PathSegment::LineTo(p) => p.hash(state),
1775 PathSegment::CurveTo {
1776 control1,
1777 control2,
1778 end,
1779 } => {
1780 control1.hash(state);
1781 control2.hash(state);
1782 end.hash(state);
1783 }
1784 PathSegment::QuadTo { control, end } => {
1785 control.hash(state);
1786 end.hash(state);
1787 }
1788 PathSegment::Arc {
1789 center,
1790 radius,
1791 start_angle,
1792 end_angle,
1793 } => {
1794 center.hash(state);
1795 (radius.round() as usize).hash(state);
1796 (start_angle.round() as usize).hash(state);
1797 (end_angle.round() as usize).hash(state);
1798 }
1799 PathSegment::Close => {} }
1801 }
1802}
1803
1804impl PartialEq for PathSegment {
1805 fn eq(&self, other: &Self) -> bool {
1806 match (self, other) {
1807 (PathSegment::MoveTo(a), PathSegment::MoveTo(b)) => a == b,
1808 (PathSegment::LineTo(a), PathSegment::LineTo(b)) => a == b,
1809 (
1810 PathSegment::CurveTo {
1811 control1: c1a,
1812 control2: c2a,
1813 end: ea,
1814 },
1815 PathSegment::CurveTo {
1816 control1: c1b,
1817 control2: c2b,
1818 end: eb,
1819 },
1820 ) => c1a == c1b && c2a == c2b && ea == eb,
1821 (
1822 PathSegment::QuadTo {
1823 control: ca,
1824 end: ea,
1825 },
1826 PathSegment::QuadTo {
1827 control: cb,
1828 end: eb,
1829 },
1830 ) => ca == cb && ea == eb,
1831 (
1832 PathSegment::Arc {
1833 center: ca,
1834 radius: ra,
1835 start_angle: sa_a,
1836 end_angle: ea_a,
1837 },
1838 PathSegment::Arc {
1839 center: cb,
1840 radius: rb,
1841 start_angle: sa_b,
1842 end_angle: ea_b,
1843 },
1844 ) => ca == cb && round_eq(*ra, *rb) && round_eq(*sa_a, *sa_b) && round_eq(*ea_a, *ea_b),
1845 (PathSegment::Close, PathSegment::Close) => true,
1846 _ => false, }
1848 }
1849}
1850
1851impl Eq for PathSegment {}
1852
1853#[derive(Debug, Clone, Hash)]
1855pub enum InlineContent {
1856 Text(StyledRun),
1857 Image(InlineImage),
1858 Shape(InlineShape),
1859 Space(InlineSpace),
1860 LineBreak(InlineBreak),
1861 Tab {
1863 style: Arc<StyleProperties>,
1864 },
1865 Marker {
1869 run: StyledRun,
1870 position_outside: bool,
1872 },
1873 Ruby {
1875 base: Vec<InlineContent>,
1876 text: Vec<InlineContent>,
1877 style: Arc<StyleProperties>,
1879 },
1880}
1881
1882#[derive(Debug, Clone)]
1883pub struct InlineImage {
1884 pub source: ImageSource,
1885 pub intrinsic_size: Size,
1886 pub display_size: Option<Size>,
1887 pub baseline_offset: f32,
1889 pub alignment: VerticalAlign,
1890 pub object_fit: ObjectFit,
1891}
1892
1893impl PartialEq for InlineImage {
1894 fn eq(&self, other: &Self) -> bool {
1895 self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
1896 && self.source == other.source
1897 && self.intrinsic_size == other.intrinsic_size
1898 && self.display_size == other.display_size
1899 && self.alignment == other.alignment
1900 && self.object_fit == other.object_fit
1901 }
1902}
1903
1904impl Eq for InlineImage {}
1905
1906impl Hash for InlineImage {
1907 fn hash<H: Hasher>(&self, state: &mut H) {
1908 self.source.hash(state);
1909 self.intrinsic_size.hash(state);
1910 self.display_size.hash(state);
1911 self.baseline_offset.to_bits().hash(state);
1912 self.alignment.hash(state);
1913 self.object_fit.hash(state);
1914 }
1915}
1916
1917impl PartialOrd for InlineImage {
1918 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1919 Some(self.cmp(other))
1920 }
1921}
1922
1923impl Ord for InlineImage {
1924 fn cmp(&self, other: &Self) -> Ordering {
1925 self.source
1926 .cmp(&other.source)
1927 .then_with(|| self.intrinsic_size.cmp(&other.intrinsic_size))
1928 .then_with(|| self.display_size.cmp(&other.display_size))
1929 .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
1930 .then_with(|| self.alignment.cmp(&other.alignment))
1931 .then_with(|| self.object_fit.cmp(&other.object_fit))
1932 }
1933}
1934
1935#[derive(Debug, Clone)]
1937pub struct Glyph {
1938 pub glyph_id: u16,
1940 pub codepoint: char,
1941 pub font_hash: u64,
1943 pub font_metrics: LayoutFontMetrics,
1945 pub style: Arc<StyleProperties>,
1946 pub source: GlyphSource,
1947
1948 pub logical_byte_index: usize,
1950 pub logical_byte_len: usize,
1951 pub content_index: usize,
1952 pub cluster: u32,
1953
1954 pub advance: f32,
1956 pub kerning: f32,
1957 pub offset: Point,
1958
1959 pub vertical_advance: f32,
1961 pub vertical_origin_y: f32, pub vertical_bearing: Point,
1963 pub orientation: GlyphOrientation,
1964
1965 pub script: Script,
1967 pub bidi_level: BidiLevel,
1968}
1969
1970impl Glyph {
1971 #[inline]
1972 fn bounds(&self) -> Rect {
1973 Rect {
1974 x: 0.0,
1975 y: 0.0,
1976 width: self.advance,
1977 height: self.style.line_height.resolve_with_metrics(self.style.font_size_px, &self.font_metrics),
1978 }
1979 }
1980
1981 #[inline]
1982 fn character_class(&self) -> CharacterClass {
1983 classify_character(self.codepoint as u32)
1984 }
1985
1986 #[inline]
1987 fn is_whitespace(&self) -> bool {
1988 self.character_class() == CharacterClass::Space
1989 }
1990
1991 #[inline]
1992 fn can_justify(&self) -> bool {
1993 !self.codepoint.is_whitespace() && self.character_class() != CharacterClass::Combining
1994 }
1995
1996 #[inline]
1997 fn justification_priority(&self) -> u8 {
1998 get_justification_priority(self.character_class())
1999 }
2000
2001 #[inline]
2002 fn break_opportunity_after(&self) -> bool {
2003 let is_whitespace = self.codepoint.is_whitespace();
2004 let is_soft_hyphen = self.codepoint == '\u{00AD}';
2005 let is_hyphen_minus = self.codepoint == '\u{002D}';
2006 let is_hyphen = self.codepoint == '\u{2010}';
2007 is_whitespace || is_soft_hyphen || is_hyphen_minus || is_hyphen
2008 }
2009}
2010
2011#[derive(Debug, Clone)]
2013pub struct TextRunInfo<'a> {
2014 pub text: &'a str,
2015 pub style: Arc<StyleProperties>,
2016 pub logical_start: usize,
2017 pub content_index: usize,
2018}
2019
2020#[derive(Debug, Clone)]
2021pub enum ImageSource {
2022 Ref(ImageRef),
2024 Url(String),
2026 Data(Arc<[u8]>),
2028 Svg(Arc<str>),
2030 Placeholder(Size),
2032}
2033
2034impl PartialEq for ImageSource {
2035 fn eq(&self, other: &Self) -> bool {
2036 match (self, other) {
2037 (ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash() == b.get_hash(),
2038 (ImageSource::Url(a), ImageSource::Url(b)) => a == b,
2039 (ImageSource::Data(a), ImageSource::Data(b)) => Arc::ptr_eq(a, b),
2040 (ImageSource::Svg(a), ImageSource::Svg(b)) => Arc::ptr_eq(a, b),
2041 (ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
2042 a.width.to_bits() == b.width.to_bits() && a.height.to_bits() == b.height.to_bits()
2043 }
2044 _ => false,
2045 }
2046 }
2047}
2048
2049impl Eq for ImageSource {}
2050
2051impl std::hash::Hash for ImageSource {
2052 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2053 core::mem::discriminant(self).hash(state);
2054 match self {
2055 ImageSource::Ref(r) => r.get_hash().hash(state),
2056 ImageSource::Url(s) => s.hash(state),
2057 ImageSource::Data(d) => (Arc::as_ptr(d) as *const u8 as usize).hash(state),
2058 ImageSource::Svg(s) => (Arc::as_ptr(s) as *const u8 as usize).hash(state),
2059 ImageSource::Placeholder(sz) => {
2060 sz.width.to_bits().hash(state);
2061 sz.height.to_bits().hash(state);
2062 }
2063 }
2064 }
2065}
2066
2067impl PartialOrd for ImageSource {
2068 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2069 Some(self.cmp(other))
2070 }
2071}
2072
2073impl Ord for ImageSource {
2074 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2075 fn variant_index(s: &ImageSource) -> u8 {
2076 match s {
2077 ImageSource::Ref(_) => 0,
2078 ImageSource::Url(_) => 1,
2079 ImageSource::Data(_) => 2,
2080 ImageSource::Svg(_) => 3,
2081 ImageSource::Placeholder(_) => 4,
2082 }
2083 }
2084 match (self, other) {
2085 (ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash().cmp(&b.get_hash()),
2086 (ImageSource::Url(a), ImageSource::Url(b)) => a.cmp(b),
2087 (ImageSource::Data(a), ImageSource::Data(b)) => {
2088 (Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
2089 }
2090 (ImageSource::Svg(a), ImageSource::Svg(b)) => {
2091 (Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
2092 }
2093 (ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
2094 (a.width.to_bits(), a.height.to_bits())
2095 .cmp(&(b.width.to_bits(), b.height.to_bits()))
2096 }
2097 _ => variant_index(self).cmp(&variant_index(other)),
2099 }
2100 }
2101}
2102
2103#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
2109pub enum VerticalAlign {
2110 #[default]
2112 Baseline,
2113 Bottom,
2115 Top,
2117 Middle,
2119 TextTop,
2121 TextBottom,
2123 Sub,
2125 Super,
2127 Offset(f32),
2129}
2130
2131impl std::hash::Hash for VerticalAlign {
2132 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2133 core::mem::discriminant(self).hash(state);
2134 if let VerticalAlign::Offset(f) = self {
2135 f.to_bits().hash(state);
2136 }
2137 }
2138}
2139
2140impl Eq for VerticalAlign {}
2141
2142impl Ord for VerticalAlign {
2143 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2144 self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
2145 }
2146}
2147
2148#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
2149pub enum ObjectFit {
2150 Fill,
2152 Contain,
2154 Cover,
2156 None,
2158 ScaleDown,
2160}
2161
2162#[derive(Debug, Clone, PartialEq)]
2168pub struct InlineBorderInfo {
2169 pub top: f32,
2171 pub right: f32,
2172 pub bottom: f32,
2173 pub left: f32,
2174 pub top_color: ColorU,
2176 pub right_color: ColorU,
2177 pub bottom_color: ColorU,
2178 pub left_color: ColorU,
2179 pub radius: Option<f32>,
2181 pub padding_top: f32,
2183 pub padding_right: f32,
2184 pub padding_bottom: f32,
2185 pub padding_left: f32,
2186 pub is_first_fragment: bool,
2191 pub is_last_fragment: bool,
2193 pub is_rtl: bool,
2197}
2198
2199impl Default for InlineBorderInfo {
2200 fn default() -> Self {
2201 Self {
2202 top: 0.0,
2203 right: 0.0,
2204 bottom: 0.0,
2205 left: 0.0,
2206 top_color: ColorU::TRANSPARENT,
2207 right_color: ColorU::TRANSPARENT,
2208 bottom_color: ColorU::TRANSPARENT,
2209 left_color: ColorU::TRANSPARENT,
2210 radius: None,
2211 padding_top: 0.0,
2212 padding_right: 0.0,
2213 padding_bottom: 0.0,
2214 padding_left: 0.0,
2215 is_first_fragment: true,
2216 is_last_fragment: true,
2217 is_rtl: false,
2218 }
2219 }
2220}
2221
2222impl InlineBorderInfo {
2223 pub fn has_border(&self) -> bool {
2225 self.top > 0.0 || self.right > 0.0 || self.bottom > 0.0 || self.left > 0.0
2226 }
2227
2228 pub fn has_chrome(&self) -> bool {
2230 self.has_border()
2231 || self.padding_top > 0.0
2232 || self.padding_right > 0.0
2233 || self.padding_bottom > 0.0
2234 || self.padding_left > 0.0
2235 }
2236
2237 pub fn left_inset(&self) -> f32 {
2246 let show = if self.is_rtl { self.is_last_fragment } else { self.is_first_fragment };
2247 if show { self.left + self.padding_left } else { 0.0 }
2248 }
2249 pub fn right_inset(&self) -> f32 {
2252 let show = if self.is_rtl { self.is_first_fragment } else { self.is_last_fragment };
2253 if show { self.right + self.padding_right } else { 0.0 }
2254 }
2255 pub fn top_inset(&self) -> f32 { self.top + self.padding_top }
2257 pub fn bottom_inset(&self) -> f32 { self.bottom + self.padding_bottom }
2259}
2260
2261#[derive(Debug, Clone)]
2262pub struct InlineShape {
2263 pub shape_def: ShapeDefinition,
2264 pub fill: Option<ColorU>,
2265 pub stroke: Option<Stroke>,
2266 pub baseline_offset: f32,
2267 pub alignment: VerticalAlign,
2270 pub source_node_id: Option<azul_core::dom::NodeId>,
2274}
2275
2276#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
2277pub enum OverflowBehavior {
2278 Visible,
2280 Hidden,
2282 Scroll,
2284 #[default]
2286 Auto,
2287 Break,
2289}
2290
2291#[derive(Debug, Clone)]
2292pub struct MeasuredImage {
2293 pub source: ImageSource,
2294 pub size: Size,
2295 pub baseline_offset: f32,
2296 pub alignment: VerticalAlign,
2297 pub content_index: usize,
2298}
2299
2300#[derive(Debug, Clone)]
2301pub struct MeasuredShape {
2302 pub shape_def: ShapeDefinition,
2303 pub size: Size,
2304 pub baseline_offset: f32,
2305 pub alignment: VerticalAlign,
2306 pub content_index: usize,
2307}
2308
2309#[derive(Debug, Clone)]
2310pub struct InlineSpace {
2311 pub width: f32,
2312 pub is_breaking: bool, pub is_stretchy: bool, }
2315
2316impl PartialEq for InlineSpace {
2317 fn eq(&self, other: &Self) -> bool {
2318 self.width.to_bits() == other.width.to_bits()
2319 && self.is_breaking == other.is_breaking
2320 && self.is_stretchy == other.is_stretchy
2321 }
2322}
2323
2324impl Eq for InlineSpace {}
2325
2326impl Hash for InlineSpace {
2327 fn hash<H: Hasher>(&self, state: &mut H) {
2328 self.width.to_bits().hash(state);
2329 self.is_breaking.hash(state);
2330 self.is_stretchy.hash(state);
2331 }
2332}
2333
2334impl PartialOrd for InlineSpace {
2335 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2336 Some(self.cmp(other))
2337 }
2338}
2339
2340impl Ord for InlineSpace {
2341 fn cmp(&self, other: &Self) -> Ordering {
2342 self.width
2343 .total_cmp(&other.width)
2344 .then_with(|| self.is_breaking.cmp(&other.is_breaking))
2345 .then_with(|| self.is_stretchy.cmp(&other.is_stretchy))
2346 }
2347}
2348
2349impl PartialEq for InlineShape {
2350 fn eq(&self, other: &Self) -> bool {
2351 self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
2352 && self.shape_def == other.shape_def
2353 && self.fill == other.fill
2354 && self.stroke == other.stroke
2355 && self.alignment == other.alignment
2356 && self.source_node_id == other.source_node_id
2357 }
2358}
2359
2360impl Eq for InlineShape {}
2361
2362impl Hash for InlineShape {
2363 fn hash<H: Hasher>(&self, state: &mut H) {
2364 self.shape_def.hash(state);
2365 self.fill.hash(state);
2366 self.stroke.hash(state);
2367 self.baseline_offset.to_bits().hash(state);
2368 self.alignment.hash(state);
2369 self.source_node_id.hash(state);
2370 }
2371}
2372
2373impl PartialOrd for InlineShape {
2374 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2375 Some(
2376 self.shape_def
2377 .partial_cmp(&other.shape_def)?
2378 .then_with(|| self.fill.cmp(&other.fill))
2379 .then_with(|| {
2380 self.stroke
2381 .partial_cmp(&other.stroke)
2382 .unwrap_or(Ordering::Equal)
2383 })
2384 .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
2385 .then_with(|| self.alignment.cmp(&other.alignment))
2386 .then_with(|| self.source_node_id.cmp(&other.source_node_id)),
2387 )
2388 }
2389}
2390
2391#[derive(Debug, Default, Clone, Copy)]
2392pub struct Rect {
2393 pub x: f32,
2394 pub y: f32,
2395 pub width: f32,
2396 pub height: f32,
2397}
2398
2399impl PartialEq for Rect {
2400 fn eq(&self, other: &Self) -> bool {
2401 round_eq(self.x, other.x)
2402 && round_eq(self.y, other.y)
2403 && round_eq(self.width, other.width)
2404 && round_eq(self.height, other.height)
2405 }
2406}
2407impl Eq for Rect {}
2408
2409impl Hash for Rect {
2410 fn hash<H: Hasher>(&self, state: &mut H) {
2411 (self.x.round() as usize).hash(state);
2414 (self.y.round() as usize).hash(state);
2415 (self.width.round() as usize).hash(state);
2416 (self.height.round() as usize).hash(state);
2417 }
2418}
2419
2420#[derive(Debug, Default, Clone, Copy, PartialOrd)]
2421pub struct Size {
2422 pub width: f32,
2423 pub height: f32,
2424}
2425
2426impl Ord for Size {
2427 fn cmp(&self, other: &Self) -> Ordering {
2428 (self.width.round() as usize)
2429 .cmp(&(other.width.round() as usize))
2430 .then_with(|| (self.height.round() as usize).cmp(&(other.height.round() as usize)))
2431 }
2432}
2433
2434impl Hash for Size {
2436 fn hash<H: Hasher>(&self, state: &mut H) {
2437 (self.width.round() as usize).hash(state);
2438 (self.height.round() as usize).hash(state);
2439 }
2440}
2441impl PartialEq for Size {
2442 fn eq(&self, other: &Self) -> bool {
2443 round_eq(self.width, other.width) && round_eq(self.height, other.height)
2444 }
2445}
2446impl Eq for Size {}
2447
2448impl Size {
2449 pub const fn zero() -> Self {
2450 Self::new(0.0, 0.0)
2451 }
2452 pub const fn new(width: f32, height: f32) -> Self {
2453 Self { width, height }
2454 }
2455}
2456
2457#[derive(Debug, Default, Clone, Copy, PartialOrd)]
2458pub struct Point {
2459 pub x: f32,
2460 pub y: f32,
2461}
2462
2463impl Hash for Point {
2465 fn hash<H: Hasher>(&self, state: &mut H) {
2466 (self.x.round() as usize).hash(state);
2467 (self.y.round() as usize).hash(state);
2468 }
2469}
2470
2471impl PartialEq for Point {
2472 fn eq(&self, other: &Self) -> bool {
2473 round_eq(self.x, other.x) && round_eq(self.y, other.y)
2474 }
2475}
2476
2477impl Eq for Point {}
2478
2479#[derive(Debug, Clone, PartialOrd)]
2480pub enum ShapeDefinition {
2481 Rectangle {
2482 size: Size,
2483 corner_radius: Option<f32>,
2484 },
2485 Circle {
2486 radius: f32,
2487 },
2488 Ellipse {
2489 radii: Size,
2490 },
2491 Polygon {
2492 points: Vec<Point>,
2493 },
2494 Path {
2495 segments: Vec<PathSegment>,
2496 },
2497}
2498
2499impl Hash for ShapeDefinition {
2501 fn hash<H: Hasher>(&self, state: &mut H) {
2502 discriminant(self).hash(state);
2503 match self {
2504 ShapeDefinition::Rectangle {
2505 size,
2506 corner_radius,
2507 } => {
2508 size.hash(state);
2509 corner_radius.map(|r| r.round() as usize).hash(state);
2510 }
2511 ShapeDefinition::Circle { radius } => {
2512 (radius.round() as usize).hash(state);
2513 }
2514 ShapeDefinition::Ellipse { radii } => {
2515 radii.hash(state);
2516 }
2517 ShapeDefinition::Polygon { points } => {
2518 points.hash(state);
2520 }
2521 ShapeDefinition::Path { segments } => {
2522 segments.hash(state);
2524 }
2525 }
2526 }
2527}
2528
2529impl PartialEq for ShapeDefinition {
2530 fn eq(&self, other: &Self) -> bool {
2531 match (self, other) {
2532 (
2533 ShapeDefinition::Rectangle {
2534 size: s1,
2535 corner_radius: r1,
2536 },
2537 ShapeDefinition::Rectangle {
2538 size: s2,
2539 corner_radius: r2,
2540 },
2541 ) => {
2542 s1 == s2
2543 && match (r1, r2) {
2544 (None, None) => true,
2545 (Some(v1), Some(v2)) => round_eq(*v1, *v2),
2546 _ => false,
2547 }
2548 }
2549 (ShapeDefinition::Circle { radius: r1 }, ShapeDefinition::Circle { radius: r2 }) => {
2550 round_eq(*r1, *r2)
2551 }
2552 (ShapeDefinition::Ellipse { radii: r1 }, ShapeDefinition::Ellipse { radii: r2 }) => {
2553 r1 == r2
2554 }
2555 (ShapeDefinition::Polygon { points: p1 }, ShapeDefinition::Polygon { points: p2 }) => {
2556 p1 == p2
2557 }
2558 (ShapeDefinition::Path { segments: s1 }, ShapeDefinition::Path { segments: s2 }) => {
2559 s1 == s2
2560 }
2561 _ => false,
2562 }
2563 }
2564}
2565impl Eq for ShapeDefinition {}
2566
2567impl ShapeDefinition {
2568 pub fn get_size(&self) -> Size {
2570 match self {
2571 ShapeDefinition::Rectangle { size, .. } => *size,
2573
2574 ShapeDefinition::Circle { radius } => {
2576 let diameter = radius * 2.0;
2577 Size::new(diameter, diameter)
2578 }
2579
2580 ShapeDefinition::Ellipse { radii } => Size::new(radii.width * 2.0, radii.height * 2.0),
2582
2583 ShapeDefinition::Polygon { points } => calculate_bounding_box_size(points),
2585
2586 ShapeDefinition::Path { segments } => {
2593 let mut points = Vec::new();
2594 let mut current_pos = Point { x: 0.0, y: 0.0 };
2595
2596 for segment in segments {
2597 match segment {
2598 PathSegment::MoveTo(p) | PathSegment::LineTo(p) => {
2599 points.push(*p);
2600 current_pos = *p;
2601 }
2602 PathSegment::QuadTo { control, end } => {
2603 points.push(current_pos);
2604 points.push(*control);
2605 points.push(*end);
2606 current_pos = *end;
2607 }
2608 PathSegment::CurveTo {
2609 control1,
2610 control2,
2611 end,
2612 } => {
2613 points.push(current_pos);
2614 points.push(*control1);
2615 points.push(*control2);
2616 points.push(*end);
2617 current_pos = *end;
2618 }
2619 PathSegment::Arc {
2620 center,
2621 radius,
2622 start_angle,
2623 end_angle,
2624 } => {
2625 let start_point = Point {
2627 x: center.x + radius * start_angle.cos(),
2628 y: center.y + radius * start_angle.sin(),
2629 };
2630 let end_point = Point {
2631 x: center.x + radius * end_angle.cos(),
2632 y: center.y + radius * end_angle.sin(),
2633 };
2634 points.push(start_point);
2635 points.push(end_point);
2636
2637 let mut normalized_end = *end_angle;
2641 while normalized_end < *start_angle {
2642 normalized_end += 2.0 * std::f32::consts::PI;
2643 }
2644
2645 let mut check_angle = (*start_angle / std::f32::consts::FRAC_PI_2)
2648 .ceil()
2649 * std::f32::consts::FRAC_PI_2;
2650
2651 while check_angle < normalized_end {
2655 points.push(Point {
2656 x: center.x + radius * check_angle.cos(),
2657 y: center.y + radius * check_angle.sin(),
2658 });
2659 check_angle += std::f32::consts::FRAC_PI_2;
2660 }
2661
2662 current_pos = end_point;
2665 }
2666 PathSegment::Close => {
2667 }
2669 }
2670 }
2671 calculate_bounding_box_size(&points)
2672 }
2673 }
2674 }
2675}
2676
2677pub(crate) fn resolve_effective_alignment(
2685 text_align: TextAlign,
2686 text_align_last: TextAlign,
2687 is_last_or_forced: bool,
2688) -> TextAlign {
2689 if is_last_or_forced {
2690 if text_align_last == TextAlign::default() {
2691 if text_align == TextAlign::Justify { TextAlign::Start } else { text_align }
2692 } else {
2693 text_align_last
2694 }
2695 } else {
2696 text_align
2697 }
2698}
2699
2700fn calculate_bounding_box_size(points: &[Point]) -> Size {
2702 if points.is_empty() {
2703 return Size::zero();
2704 }
2705
2706 let mut min_x = f32::MAX;
2707 let mut max_x = f32::MIN;
2708 let mut min_y = f32::MAX;
2709 let mut max_y = f32::MIN;
2710
2711 for point in points {
2712 min_x = min_x.min(point.x);
2713 max_x = max_x.max(point.x);
2714 min_y = min_y.min(point.y);
2715 max_y = max_y.max(point.y);
2716 }
2717
2718 if min_x > max_x || min_y > max_y {
2720 return Size::zero();
2721 }
2722
2723 Size::new(max_x - min_x, max_y - min_y)
2724}
2725
2726#[derive(Debug, Clone, PartialOrd)]
2727pub struct Stroke {
2728 pub color: ColorU,
2729 pub width: f32,
2730 pub dash_pattern: Option<Vec<f32>>,
2731}
2732
2733impl Hash for Stroke {
2735 fn hash<H: Hasher>(&self, state: &mut H) {
2736 self.color.hash(state);
2737 (self.width.round() as usize).hash(state);
2738
2739 match &self.dash_pattern {
2741 None => 0u8.hash(state), Some(pattern) => {
2743 1u8.hash(state); pattern.len().hash(state); for &val in pattern {
2746 (val.round() as usize).hash(state); }
2748 }
2749 }
2750 }
2751}
2752
2753impl PartialEq for Stroke {
2754 fn eq(&self, other: &Self) -> bool {
2755 if self.color != other.color || !round_eq(self.width, other.width) {
2756 return false;
2757 }
2758 match (&self.dash_pattern, &other.dash_pattern) {
2759 (None, None) => true,
2760 (Some(p1), Some(p2)) => {
2761 p1.len() == p2.len() && p1.iter().zip(p2.iter()).all(|(a, b)| round_eq(*a, *b))
2762 }
2763 _ => false,
2764 }
2765 }
2766}
2767
2768impl Eq for Stroke {}
2769
2770fn round_eq(a: f32, b: f32) -> bool {
2772 (a.round() as isize) == (b.round() as isize)
2773}
2774
2775#[derive(Debug, Clone)]
2776pub enum ShapeBoundary {
2777 Rectangle(Rect),
2778 Circle { center: Point, radius: f32 },
2779 Ellipse { center: Point, radii: Size },
2780 Polygon { points: Vec<Point> },
2781 Path { segments: Vec<PathSegment> },
2782}
2783
2784impl ShapeBoundary {
2785 pub fn inflate(&self, margin: f32) -> Self {
2786 if margin == 0.0 {
2787 return self.clone();
2788 }
2789 match self {
2790 Self::Rectangle(rect) => Self::Rectangle(Rect {
2791 x: rect.x - margin,
2792 y: rect.y - margin,
2793 width: (rect.width + margin * 2.0).max(0.0),
2794 height: (rect.height + margin * 2.0).max(0.0),
2795 }),
2796 Self::Circle { center, radius } => Self::Circle {
2797 center: *center,
2798 radius: radius + margin,
2799 },
2800 _ => self.clone(),
2803 }
2804 }
2805}
2806
2807impl Hash for ShapeBoundary {
2809 fn hash<H: Hasher>(&self, state: &mut H) {
2810 discriminant(self).hash(state);
2811 match self {
2812 ShapeBoundary::Rectangle(rect) => rect.hash(state),
2813 ShapeBoundary::Circle { center, radius } => {
2814 center.hash(state);
2815 (radius.round() as usize).hash(state);
2816 }
2817 ShapeBoundary::Ellipse { center, radii } => {
2818 center.hash(state);
2819 radii.hash(state);
2820 }
2821 ShapeBoundary::Polygon { points } => points.hash(state),
2822 ShapeBoundary::Path { segments } => segments.hash(state),
2823 }
2824 }
2825}
2826impl PartialEq for ShapeBoundary {
2827 fn eq(&self, other: &Self) -> bool {
2828 match (self, other) {
2829 (ShapeBoundary::Rectangle(r1), ShapeBoundary::Rectangle(r2)) => r1 == r2,
2830 (
2831 ShapeBoundary::Circle {
2832 center: c1,
2833 radius: r1,
2834 },
2835 ShapeBoundary::Circle {
2836 center: c2,
2837 radius: r2,
2838 },
2839 ) => c1 == c2 && round_eq(*r1, *r2),
2840 (
2841 ShapeBoundary::Ellipse {
2842 center: c1,
2843 radii: r1,
2844 },
2845 ShapeBoundary::Ellipse {
2846 center: c2,
2847 radii: r2,
2848 },
2849 ) => c1 == c2 && r1 == r2,
2850 (ShapeBoundary::Polygon { points: p1 }, ShapeBoundary::Polygon { points: p2 }) => {
2851 p1 == p2
2852 }
2853 (ShapeBoundary::Path { segments: s1 }, ShapeBoundary::Path { segments: s2 }) => {
2854 s1 == s2
2855 }
2856 _ => false,
2857 }
2858 }
2859}
2860impl Eq for ShapeBoundary {}
2861
2862impl ShapeBoundary {
2863 pub fn from_css_shape(
2872 css_shape: &azul_css::shape::CssShape,
2873 reference_box: Rect,
2874 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2875 ) -> Self {
2876 use azul_css::shape::CssShape;
2877
2878 if let Some(msgs) = debug_messages {
2879 msgs.push(LayoutDebugMessage::info(format!(
2880 "[ShapeBoundary::from_css_shape] Input CSS shape: {:?}",
2881 css_shape
2882 )));
2883 msgs.push(LayoutDebugMessage::info(format!(
2884 "[ShapeBoundary::from_css_shape] Reference box: {:?}",
2885 reference_box
2886 )));
2887 }
2888
2889 let result = match css_shape {
2890 CssShape::Circle(circle) => {
2891 let center = Point {
2892 x: reference_box.x + circle.center.x,
2893 y: reference_box.y + circle.center.y,
2894 };
2895 if let Some(msgs) = debug_messages {
2896 msgs.push(LayoutDebugMessage::info(format!(
2897 "[ShapeBoundary::from_css_shape] Circle - CSS center: ({}, {}), radius: {}",
2898 circle.center.x, circle.center.y, circle.radius
2899 )));
2900 msgs.push(LayoutDebugMessage::info(format!(
2901 "[ShapeBoundary::from_css_shape] Circle - Absolute center: ({}, {}), \
2902 radius: {}",
2903 center.x, center.y, circle.radius
2904 )));
2905 }
2906 ShapeBoundary::Circle {
2907 center,
2908 radius: circle.radius,
2909 }
2910 }
2911
2912 CssShape::Ellipse(ellipse) => {
2913 let center = Point {
2914 x: reference_box.x + ellipse.center.x,
2915 y: reference_box.y + ellipse.center.y,
2916 };
2917 let radii = Size {
2918 width: ellipse.radius_x,
2919 height: ellipse.radius_y,
2920 };
2921 if let Some(msgs) = debug_messages {
2922 msgs.push(LayoutDebugMessage::info(format!(
2923 "[ShapeBoundary::from_css_shape] Ellipse - center: ({}, {}), radii: ({}, \
2924 {})",
2925 center.x, center.y, radii.width, radii.height
2926 )));
2927 }
2928 ShapeBoundary::Ellipse { center, radii }
2929 }
2930
2931 CssShape::Polygon(polygon) => {
2932 let points = polygon
2933 .points
2934 .as_ref()
2935 .iter()
2936 .map(|pt| Point {
2937 x: reference_box.x + pt.x,
2938 y: reference_box.y + pt.y,
2939 })
2940 .collect();
2941 if let Some(msgs) = debug_messages {
2942 msgs.push(LayoutDebugMessage::info(format!(
2943 "[ShapeBoundary::from_css_shape] Polygon - {} points",
2944 polygon.points.as_ref().len()
2945 )));
2946 }
2947 ShapeBoundary::Polygon { points }
2948 }
2949
2950 CssShape::Inset(inset) => {
2951 let x = reference_box.x + inset.inset_left;
2953 let y = reference_box.y + inset.inset_top;
2954 let width = reference_box.width - inset.inset_left - inset.inset_right;
2955 let height = reference_box.height - inset.inset_top - inset.inset_bottom;
2956
2957 if let Some(msgs) = debug_messages {
2958 msgs.push(LayoutDebugMessage::info(format!(
2959 "[ShapeBoundary::from_css_shape] Inset - insets: ({}, {}, {}, {})",
2960 inset.inset_top, inset.inset_right, inset.inset_bottom, inset.inset_left
2961 )));
2962 msgs.push(LayoutDebugMessage::info(format!(
2963 "[ShapeBoundary::from_css_shape] Inset - resulting rect: x={}, y={}, \
2964 w={}, h={}",
2965 x, y, width, height
2966 )));
2967 }
2968
2969 ShapeBoundary::Rectangle(Rect {
2970 x,
2971 y,
2972 width: width.max(0.0),
2973 height: height.max(0.0),
2974 })
2975 }
2976
2977 CssShape::Path(path) => {
2978 if let Some(msgs) = debug_messages {
2979 msgs.push(LayoutDebugMessage::info(
2980 "[ShapeBoundary::from_css_shape] Path - fallback to rectangle".to_string(),
2981 ));
2982 }
2983 ShapeBoundary::Rectangle(reference_box)
2986 }
2987 };
2988
2989 if let Some(msgs) = debug_messages {
2990 msgs.push(LayoutDebugMessage::info(format!(
2991 "[ShapeBoundary::from_css_shape] Result: {:?}",
2992 result
2993 )));
2994 }
2995 result
2996 }
2997}
2998
2999#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3000pub struct InlineBreak {
3001 pub break_type: BreakType,
3002 pub clear: ClearType,
3003 pub content_index: usize,
3004}
3005
3006#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3008pub enum BreakType {
3009 Soft, Hard, Page, Column, }
3014
3015#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3016pub enum ClearType {
3017 None,
3018 Left,
3019 Right,
3020 Both,
3021}
3022
3023#[derive(Debug, Clone)]
3025pub struct ShapeConstraints {
3026 pub boundaries: Vec<ShapeBoundary>,
3027 pub exclusions: Vec<ShapeBoundary>,
3028 pub writing_mode: WritingMode,
3029 pub text_align: TextAlign,
3030 pub line_height: LineHeight,
3031}
3032
3033#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
3034pub enum WritingMode {
3035 #[default]
3036 HorizontalTb, VerticalRl, VerticalLr, SidewaysRl, SidewaysLr, }
3042
3043impl WritingMode {
3044 pub fn is_advance_horizontal(&self) -> bool {
3046 matches!(
3047 self,
3048 WritingMode::HorizontalTb | WritingMode::SidewaysRl | WritingMode::SidewaysLr
3049 )
3050 }
3051}
3052
3053#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
3054pub enum JustifyContent {
3055 #[default]
3056 None,
3057 InterWord, InterCharacter, Distribute, Kashida, }
3062
3063#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
3065pub enum TextAlign {
3066 #[default]
3067 Left,
3068 Right,
3069 Center,
3070 Justify,
3071 Start,
3072 End, JustifyAll, }
3075
3076#[derive(Debug, Clone, Copy, PartialEq, Default, Eq, PartialOrd, Ord, Hash)]
3079pub enum TextOrientation {
3080 #[default]
3081 Mixed, Upright, Sideways, }
3085
3086#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3087pub struct TextDecoration {
3088 pub underline: bool,
3089 pub strikethrough: bool,
3090 pub overline: bool,
3091}
3092
3093impl Default for TextDecoration {
3094 fn default() -> Self {
3095 TextDecoration {
3096 underline: false,
3097 overline: false,
3098 strikethrough: false,
3099 }
3100 }
3101}
3102
3103impl TextDecoration {
3104 pub fn from_css(css: azul_css::props::style::text::StyleTextDecoration) -> Self {
3110 use azul_css::props::style::text::StyleTextDecoration;
3111 match css {
3112 StyleTextDecoration::None => Self::default(),
3113 StyleTextDecoration::Underline => Self {
3114 underline: true,
3115 strikethrough: false,
3116 overline: false,
3117 },
3118 StyleTextDecoration::Overline => Self {
3119 underline: false,
3120 strikethrough: false,
3121 overline: true,
3122 },
3123 StyleTextDecoration::LineThrough => Self {
3124 underline: false,
3125 strikethrough: true,
3126 overline: false,
3127 },
3128 }
3129 }
3130}
3131
3132#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
3133pub enum TextTransform {
3134 #[default]
3135 None,
3136 Uppercase,
3137 Lowercase,
3138 Capitalize,
3139 FullWidth,
3141}
3142
3143pub type FourCc = [u8; 4];
3145
3146#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
3148pub enum Spacing {
3149 Px(i32), Em(f32),
3151}
3152
3153impl Eq for Spacing {}
3157
3158impl Hash for Spacing {
3159 fn hash<H: Hasher>(&self, state: &mut H) {
3160 discriminant(self).hash(state);
3162 match self {
3163 Spacing::Px(val) => val.hash(state),
3164 Spacing::Em(val) => val.to_bits().hash(state),
3167 }
3168 }
3169}
3170
3171impl Default for Spacing {
3172 fn default() -> Self {
3173 Spacing::Px(0)
3174 }
3175}
3176
3177impl Default for FontHash {
3178 fn default() -> Self {
3179 Self::invalid()
3180 }
3181}
3182
3183#[derive(Debug, Clone, PartialEq)]
3185pub struct StyleProperties {
3186 pub font_stack: FontStack,
3190 pub font_size_px: f32,
3191 pub color: ColorU,
3192 pub background_color: Option<ColorU>,
3203 pub background_content: Vec<StyleBackgroundContent>,
3206 pub border: Option<InlineBorderInfo>,
3208 pub letter_spacing: Spacing,
3210 pub word_spacing: Spacing,
3211
3212 pub line_height: LineHeight,
3213 pub text_decoration: TextDecoration,
3214
3215 pub font_features: Vec<String>,
3217
3218 pub font_variations: Vec<(FourCc, f32)>,
3220 pub tab_size: f32,
3222 pub text_transform: TextTransform,
3224 pub writing_mode: WritingMode,
3226 pub text_orientation: TextOrientation,
3227 pub text_combine_upright: Option<TextCombineUpright>,
3229
3230 pub font_variant_caps: FontVariantCaps,
3232 pub font_variant_numeric: FontVariantNumeric,
3233 pub font_variant_ligatures: FontVariantLigatures,
3234 pub font_variant_east_asian: FontVariantEastAsian,
3235}
3236
3237impl Default for StyleProperties {
3238 fn default() -> Self {
3239 const FONT_SIZE: f32 = 16.0;
3240 const TAB_SIZE: f32 = 8.0;
3241 Self {
3242 font_stack: FontStack::default(),
3243 font_size_px: FONT_SIZE,
3244 color: ColorU::default(),
3245 background_color: None,
3246 background_content: Vec::new(),
3247 border: None,
3248 letter_spacing: Spacing::default(), word_spacing: Spacing::default(), line_height: LineHeight::Normal,
3251 text_decoration: TextDecoration::default(),
3252 font_features: Vec::new(),
3253 font_variations: Vec::new(),
3254 tab_size: TAB_SIZE, text_transform: TextTransform::default(),
3256 writing_mode: WritingMode::default(),
3257 text_orientation: TextOrientation::default(),
3258 text_combine_upright: None,
3259 font_variant_caps: FontVariantCaps::default(),
3260 font_variant_numeric: FontVariantNumeric::default(),
3261 font_variant_ligatures: FontVariantLigatures::default(),
3262 font_variant_east_asian: FontVariantEastAsian::default(),
3263 }
3264 }
3265}
3266
3267impl Hash for StyleProperties {
3268 fn hash<H: Hasher>(&self, state: &mut H) {
3269 self.font_stack.hash(state);
3270 self.color.hash(state);
3271 self.background_color.hash(state);
3272 self.text_decoration.hash(state);
3273 self.font_features.hash(state);
3274 self.writing_mode.hash(state);
3275 self.text_orientation.hash(state);
3276 self.text_combine_upright.hash(state);
3277 self.letter_spacing.hash(state);
3278 self.word_spacing.hash(state);
3279
3280 (self.font_size_px.round() as usize).hash(state);
3282 self.line_height.hash(state);
3283 }
3284}
3285
3286impl StyleProperties {
3287 pub fn layout_hash(&self) -> u64 {
3306 use std::hash::Hasher;
3307 let mut hasher = std::collections::hash_map::DefaultHasher::new();
3308
3309 self.font_stack.hash(&mut hasher);
3311 (self.font_size_px.round() as usize).hash(&mut hasher);
3312 self.font_features.hash(&mut hasher);
3313 for (tag, value) in &self.font_variations {
3315 tag.hash(&mut hasher);
3316 (value.round() as i32).hash(&mut hasher);
3317 }
3318
3319 self.letter_spacing.hash(&mut hasher);
3321 self.word_spacing.hash(&mut hasher);
3322 self.line_height.hash(&mut hasher);
3323 (self.tab_size.round() as usize).hash(&mut hasher);
3324
3325 self.writing_mode.hash(&mut hasher);
3327 self.text_orientation.hash(&mut hasher);
3328 self.text_combine_upright.hash(&mut hasher);
3329
3330 self.text_transform.hash(&mut hasher);
3332
3333 self.font_variant_caps.hash(&mut hasher);
3335 self.font_variant_numeric.hash(&mut hasher);
3336 self.font_variant_ligatures.hash(&mut hasher);
3337 self.font_variant_east_asian.hash(&mut hasher);
3338
3339 hasher.finish()
3340 }
3341
3342 pub fn layout_eq(&self, other: &Self) -> bool {
3351 self.layout_hash() == other.layout_hash()
3352 }
3353}
3354
3355#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
3356pub enum TextCombineUpright {
3357 None,
3358 All, Digits(u8), }
3361
3362#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3363pub enum GlyphSource {
3364 Char,
3366 Hyphen,
3368}
3369
3370#[derive(Debug, Clone, Copy, PartialEq)]
3371pub enum CharacterClass {
3372 Space, Punctuation, Letter, Ideograph, Symbol, Combining, }
3379
3380#[derive(Debug, Clone, Copy, PartialEq)]
3381pub enum GlyphOrientation {
3382 Horizontal, Vertical, Upright, Mixed, }
3387
3388#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3390pub enum BidiDirection {
3391 Ltr,
3392 Rtl,
3393}
3394
3395impl BidiDirection {
3396 pub fn is_rtl(&self) -> bool {
3397 matches!(self, BidiDirection::Rtl)
3398 }
3399}
3400
3401#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3406pub enum UnicodeBidi {
3407 Normal,
3408 Embed,
3409 Isolate,
3410 BidiOverride,
3411 IsolateOverride,
3412 Plaintext,
3413}
3414
3415impl Default for UnicodeBidi {
3416 fn default() -> Self {
3417 UnicodeBidi::Normal
3418 }
3419}
3420
3421#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
3422pub enum FontVariantCaps {
3423 #[default]
3424 Normal,
3425 SmallCaps,
3426 AllSmallCaps,
3427 PetiteCaps,
3428 AllPetiteCaps,
3429 Unicase,
3430 TitlingCaps,
3431}
3432
3433#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
3434pub enum FontVariantNumeric {
3435 #[default]
3436 Normal,
3437 LiningNums,
3438 OldstyleNums,
3439 ProportionalNums,
3440 TabularNums,
3441 DiagonalFractions,
3442 StackedFractions,
3443 Ordinal,
3444 SlashedZero,
3445}
3446
3447#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
3448pub enum FontVariantLigatures {
3449 #[default]
3450 Normal,
3451 None,
3452 Common,
3453 NoCommon,
3454 Discretionary,
3455 NoDiscretionary,
3456 Historical,
3457 NoHistorical,
3458 Contextual,
3459 NoContextual,
3460}
3461
3462#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
3463pub enum FontVariantEastAsian {
3464 #[default]
3465 Normal,
3466 Jis78,
3467 Jis83,
3468 Jis90,
3469 Jis04,
3470 Simplified,
3471 Traditional,
3472 FullWidth,
3473 ProportionalWidth,
3474 Ruby,
3475}
3476
3477#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3478pub struct BidiLevel(u8);
3479
3480impl BidiLevel {
3481 pub fn new(level: u8) -> Self {
3482 Self(level)
3483 }
3484 pub fn is_rtl(&self) -> bool {
3485 self.0 % 2 == 1
3486 }
3487 pub fn level(&self) -> u8 {
3488 self.0
3489 }
3490}
3491
3492#[derive(Debug, Clone)]
3494pub struct StyleOverride {
3495 pub target: ContentIndex,
3497 pub style: PartialStyleProperties,
3500}
3501
3502#[derive(Debug, Clone, Default)]
3503pub struct PartialStyleProperties {
3504 pub font_stack: Option<FontStack>,
3505 pub font_size_px: Option<f32>,
3506 pub color: Option<ColorU>,
3507 pub letter_spacing: Option<Spacing>,
3508 pub word_spacing: Option<Spacing>,
3509 pub line_height: Option<LineHeight>,
3510 pub text_decoration: Option<TextDecoration>,
3511 pub font_features: Option<Vec<String>>,
3512 pub font_variations: Option<Vec<(FourCc, f32)>>,
3513 pub tab_size: Option<f32>,
3514 pub text_transform: Option<TextTransform>,
3515 pub writing_mode: Option<WritingMode>,
3516 pub text_orientation: Option<TextOrientation>,
3517 pub text_combine_upright: Option<Option<TextCombineUpright>>,
3518 pub font_variant_caps: Option<FontVariantCaps>,
3519 pub font_variant_numeric: Option<FontVariantNumeric>,
3520 pub font_variant_ligatures: Option<FontVariantLigatures>,
3521 pub font_variant_east_asian: Option<FontVariantEastAsian>,
3522}
3523
3524impl Hash for PartialStyleProperties {
3525 fn hash<H: Hasher>(&self, state: &mut H) {
3526 self.font_stack.hash(state);
3527 self.font_size_px.map(|f| f.to_bits()).hash(state);
3528 self.color.hash(state);
3529 self.letter_spacing.hash(state);
3530 self.word_spacing.hash(state);
3531 self.line_height.hash(state);
3532 self.text_decoration.hash(state);
3533 self.font_features.hash(state);
3534
3535 self.font_variations.as_ref().map(|v| {
3537 for (tag, val) in v {
3538 tag.hash(state);
3539 val.to_bits().hash(state);
3540 }
3541 });
3542
3543 self.tab_size.map(|f| f.to_bits()).hash(state);
3544 self.text_transform.hash(state);
3545 self.writing_mode.hash(state);
3546 self.text_orientation.hash(state);
3547 self.text_combine_upright.hash(state);
3548 self.font_variant_caps.hash(state);
3549 self.font_variant_numeric.hash(state);
3550 self.font_variant_ligatures.hash(state);
3551 self.font_variant_east_asian.hash(state);
3552 }
3553}
3554
3555impl PartialEq for PartialStyleProperties {
3556 fn eq(&self, other: &Self) -> bool {
3557 self.font_stack == other.font_stack &&
3558 self.font_size_px.map(|f| f.to_bits()) == other.font_size_px.map(|f| f.to_bits()) &&
3559 self.color == other.color &&
3560 self.letter_spacing == other.letter_spacing &&
3561 self.word_spacing == other.word_spacing &&
3562 self.line_height == other.line_height &&
3563 self.text_decoration == other.text_decoration &&
3564 self.font_features == other.font_features &&
3565 self.font_variations == other.font_variations && self.tab_size.map(|f| f.to_bits()) == other.tab_size.map(|f| f.to_bits()) &&
3567 self.text_transform == other.text_transform &&
3568 self.writing_mode == other.writing_mode &&
3569 self.text_orientation == other.text_orientation &&
3570 self.text_combine_upright == other.text_combine_upright &&
3571 self.font_variant_caps == other.font_variant_caps &&
3572 self.font_variant_numeric == other.font_variant_numeric &&
3573 self.font_variant_ligatures == other.font_variant_ligatures &&
3574 self.font_variant_east_asian == other.font_variant_east_asian
3575 }
3576}
3577
3578impl Eq for PartialStyleProperties {}
3579
3580impl StyleProperties {
3581 fn apply_override(&self, partial: &PartialStyleProperties) -> Self {
3582 let mut new_style = self.clone();
3583 if let Some(val) = &partial.font_stack {
3584 new_style.font_stack = val.clone();
3585 }
3586 if let Some(val) = partial.font_size_px {
3587 new_style.font_size_px = val;
3588 }
3589 if let Some(val) = &partial.color {
3590 new_style.color = val.clone();
3591 }
3592 if let Some(val) = partial.letter_spacing {
3593 new_style.letter_spacing = val;
3594 }
3595 if let Some(val) = partial.word_spacing {
3596 new_style.word_spacing = val;
3597 }
3598 if let Some(val) = partial.line_height {
3599 new_style.line_height = val;
3600 }
3601 if let Some(val) = &partial.text_decoration {
3602 new_style.text_decoration = val.clone();
3603 }
3604 if let Some(val) = &partial.font_features {
3605 new_style.font_features = val.clone();
3606 }
3607 if let Some(val) = &partial.font_variations {
3608 new_style.font_variations = val.clone();
3609 }
3610 if let Some(val) = partial.tab_size {
3611 new_style.tab_size = val;
3612 }
3613 if let Some(val) = partial.text_transform {
3614 new_style.text_transform = val;
3615 }
3616 if let Some(val) = partial.writing_mode {
3617 new_style.writing_mode = val;
3618 }
3619 if let Some(val) = partial.text_orientation {
3620 new_style.text_orientation = val;
3621 }
3622 if let Some(val) = &partial.text_combine_upright {
3623 new_style.text_combine_upright = val.clone();
3624 }
3625 if let Some(val) = partial.font_variant_caps {
3626 new_style.font_variant_caps = val;
3627 }
3628 if let Some(val) = partial.font_variant_numeric {
3629 new_style.font_variant_numeric = val;
3630 }
3631 if let Some(val) = partial.font_variant_ligatures {
3632 new_style.font_variant_ligatures = val;
3633 }
3634 if let Some(val) = partial.font_variant_east_asian {
3635 new_style.font_variant_east_asian = val;
3636 }
3637 new_style
3638 }
3639}
3640
3641#[derive(Debug, Clone, Copy, PartialEq)]
3643pub enum GlyphKind {
3644 Character,
3646 Hyphen,
3648 NotDef,
3650 Kashida {
3652 width: f32,
3654 },
3655}
3656
3657#[derive(Debug, Clone)]
3660pub enum LogicalItem {
3661 Text {
3662 source: ContentIndex,
3664 text: String,
3666 style: Arc<StyleProperties>,
3667 marker_position_outside: Option<bool>,
3671 source_node_id: Option<NodeId>,
3674 },
3675 CombinedText {
3678 source: ContentIndex,
3679 text: String,
3680 style: Arc<StyleProperties>,
3681 },
3682 Ruby {
3683 source: ContentIndex,
3684 base_text: String,
3687 ruby_text: String,
3688 style: Arc<StyleProperties>,
3689 },
3690 Object {
3691 source: ContentIndex,
3693 content: InlineContent,
3695 },
3696 Tab {
3697 source: ContentIndex,
3698 style: Arc<StyleProperties>,
3699 },
3700 Break {
3701 source: ContentIndex,
3702 break_info: InlineBreak,
3703 },
3704}
3705
3706impl Hash for LogicalItem {
3707 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3708 discriminant(self).hash(state);
3709 match self {
3710 LogicalItem::Text {
3711 source,
3712 text,
3713 style,
3714 marker_position_outside,
3715 source_node_id,
3716 } => {
3717 source.hash(state);
3718 text.hash(state);
3719 style.as_ref().hash(state); marker_position_outside.hash(state);
3721 source_node_id.hash(state);
3722 }
3723 LogicalItem::CombinedText {
3724 source,
3725 text,
3726 style,
3727 } => {
3728 source.hash(state);
3729 text.hash(state);
3730 style.as_ref().hash(state);
3731 }
3732 LogicalItem::Ruby {
3733 source,
3734 base_text,
3735 ruby_text,
3736 style,
3737 } => {
3738 source.hash(state);
3739 base_text.hash(state);
3740 ruby_text.hash(state);
3741 style.as_ref().hash(state);
3742 }
3743 LogicalItem::Object { source, content } => {
3744 source.hash(state);
3745 content.hash(state);
3746 }
3747 LogicalItem::Tab { source, style } => {
3748 source.hash(state);
3749 style.as_ref().hash(state);
3750 }
3751 LogicalItem::Break { source, break_info } => {
3752 source.hash(state);
3753 break_info.hash(state);
3754 }
3755 }
3756 }
3757}
3758
3759#[derive(Debug, Clone)]
3762pub struct VisualItem {
3763 pub logical_source: LogicalItem,
3766 pub bidi_level: BidiLevel,
3768 pub script: Script,
3770 pub text: String,
3772}
3773
3774#[derive(Debug, Clone)]
3777pub enum ShapedItem {
3778 Cluster(ShapedCluster),
3779 CombinedBlock {
3782 source: ContentIndex,
3783 glyphs: ShapedGlyphVec,
3785 bounds: Rect,
3786 baseline_offset: f32,
3787 },
3788 Object {
3789 source: ContentIndex,
3790 bounds: Rect,
3791 baseline_offset: f32,
3792 content: InlineContent,
3794 },
3795 Tab {
3796 source: ContentIndex,
3797 bounds: Rect,
3798 },
3799 Break {
3800 source: ContentIndex,
3801 break_info: InlineBreak,
3802 },
3803}
3804
3805impl ShapedItem {
3806 pub fn as_cluster(&self) -> Option<&ShapedCluster> {
3807 match self {
3808 ShapedItem::Cluster(c) => Some(c),
3809 _ => None,
3810 }
3811 }
3812 pub fn bounds(&self) -> Rect {
3818 match self {
3819 ShapedItem::Cluster(cluster) => {
3820 let width = cluster.advance;
3822
3823 let (ascent, descent) = get_item_vertical_metrics_approx(self);
3827 let height = ascent + descent;
3828
3829 Rect {
3830 x: 0.0,
3831 y: 0.0,
3832 width,
3833 height,
3834 }
3835 }
3836 ShapedItem::CombinedBlock { bounds, .. } => *bounds,
3839 ShapedItem::Object { bounds, .. } => *bounds,
3840 ShapedItem::Tab { bounds, .. } => *bounds,
3841
3842 ShapedItem::Break { .. } => Rect::default(), }
3845 }
3846}
3847
3848#[derive(Debug, Clone)]
3850pub struct ShapedCluster {
3851 pub text: String,
3854 pub source_cluster_id: GraphemeClusterId,
3856 pub source_content_index: ContentIndex,
3858 pub source_node_id: Option<NodeId>,
3861 pub glyphs: ShapedGlyphVec,
3865 pub advance: f32,
3867 pub direction: BidiDirection,
3869 pub style: Arc<StyleProperties>,
3871 pub marker_position_outside: Option<bool>,
3875 pub is_first_fragment: bool,
3880 pub is_last_fragment: bool,
3883}
3884
3885#[derive(Debug, Clone)]
3887pub struct ShapedGlyph {
3888 pub kind: GlyphKind,
3890 pub glyph_id: u16,
3892 pub cluster_offset: u32,
3894 pub advance: f32,
3897 pub kerning: f32,
3900 pub offset: Point,
3902 pub vertical_advance: f32,
3904 pub vertical_offset: Point,
3906 pub script: Script,
3907 pub style: Arc<StyleProperties>,
3908 pub font_hash: u64,
3910 pub font_metrics: LayoutFontMetrics,
3912}
3913
3914impl ShapedGlyph {
3915 pub fn into_glyph_instance<T: ParsedFontTrait>(
3916 &self,
3917 writing_mode: WritingMode,
3918 loaded_fonts: &LoadedFonts<T>,
3919 ) -> GlyphInstance {
3920 let size = loaded_fonts
3921 .get_by_hash(self.font_hash)
3922 .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
3923 .unwrap_or_default();
3924
3925 let position = if writing_mode.is_advance_horizontal() {
3926 LogicalPosition {
3927 x: self.offset.x,
3928 y: self.offset.y,
3929 }
3930 } else {
3931 LogicalPosition {
3932 x: self.vertical_offset.x,
3933 y: self.vertical_offset.y,
3934 }
3935 };
3936
3937 GlyphInstance {
3938 index: self.glyph_id as u32,
3939 point: position,
3940 size,
3941 }
3942 }
3943
3944 pub fn into_glyph_instance_at<T: ParsedFontTrait>(
3947 &self,
3948 writing_mode: WritingMode,
3949 absolute_position: LogicalPosition,
3950 loaded_fonts: &LoadedFonts<T>,
3951 ) -> GlyphInstance {
3952 let size = loaded_fonts
3953 .get_by_hash(self.font_hash)
3954 .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
3955 .unwrap_or_default();
3956
3957 GlyphInstance {
3958 index: self.glyph_id as u32,
3959 point: absolute_position,
3960 size,
3961 }
3962 }
3963
3964 pub fn into_glyph_instance_at_simple(
3968 &self,
3969 _writing_mode: WritingMode,
3970 absolute_position: LogicalPosition,
3971 ) -> GlyphInstance {
3972 GlyphInstance {
3975 index: self.glyph_id as u32,
3976 point: absolute_position,
3977 size: LogicalSize::default(),
3978 }
3979 }
3980}
3981
3982#[derive(Debug, Clone)]
3985pub struct PositionedItem {
3986 pub item: ShapedItem,
3987 pub position: Point,
3988 pub line_index: usize,
3989}
3990
3991#[derive(Debug, Clone)]
3992pub struct UnifiedLayout {
3993 pub items: Vec<PositionedItem>,
3994 pub overflow: OverflowInfo,
3996}
3997
3998impl UnifiedLayout {
3999 pub fn bounds(&self) -> Rect {
4002 if self.items.is_empty() {
4003 return Rect::default();
4004 }
4005
4006 let mut min_x = f32::MAX;
4007 let mut min_y = f32::MAX;
4008 let mut max_x = f32::MIN;
4009 let mut max_y = f32::MIN;
4010
4011 for item in &self.items {
4012 let item_x = item.position.x;
4013 let item_y = item.position.y;
4014
4015 let item_bounds = item.item.bounds();
4017 let item_width = item_bounds.width;
4018 let item_height = item_bounds.height;
4019
4020 min_x = min_x.min(item_x);
4021 min_y = min_y.min(item_y);
4022 max_x = max_x.max(item_x + item_width);
4023 max_y = max_y.max(item_y + item_height);
4024 }
4025
4026 Rect {
4027 x: min_x,
4028 y: min_y,
4029 width: max_x - min_x,
4030 height: max_y - min_y,
4031 }
4032 }
4033
4034 pub fn is_empty(&self) -> bool {
4035 self.items.is_empty()
4036 }
4037 pub fn first_baseline(&self) -> Option<f32> {
4038 self.items
4039 .iter()
4040 .find_map(|item| get_baseline_for_item(&item.item))
4041 }
4042
4043 pub fn last_baseline(&self) -> Option<f32> {
4044 self.items
4045 .iter()
4046 .rev()
4047 .find_map(|item| get_baseline_for_item(&item.item))
4048 }
4049
4050 pub fn hittest_cursor(&self, point: LogicalPosition) -> Option<TextCursor> {
4056 if self.items.is_empty() {
4057 return None;
4058 }
4059
4060 let mut closest_item_idx = 0;
4062 let mut closest_distance = f32::MAX;
4063
4064 for (idx, item) in self.items.iter().enumerate() {
4065 if !matches!(item.item, ShapedItem::Cluster(_)) {
4067 continue;
4068 }
4069
4070 let item_bounds = item.item.bounds();
4071 let item_center_y = item.position.y + item_bounds.height / 2.0;
4072
4073 let vertical_distance = (point.y - item_center_y).abs();
4075
4076 let horizontal_distance = if point.x < item.position.x {
4078 item.position.x - point.x
4079 } else if point.x > item.position.x + item_bounds.width {
4080 point.x - (item.position.x + item_bounds.width)
4081 } else {
4082 0.0 };
4084
4085 let distance = vertical_distance * 2.0 + horizontal_distance;
4087
4088 if distance < closest_distance {
4089 closest_distance = distance;
4090 closest_item_idx = idx;
4091 }
4092 }
4093
4094 let closest_item = &self.items[closest_item_idx];
4096 let cluster = match &closest_item.item {
4097 ShapedItem::Cluster(c) => c,
4098 ShapedItem::Object { source, .. } | ShapedItem::CombinedBlock { source, .. } => {
4100 return Some(TextCursor {
4101 cluster_id: GraphemeClusterId {
4102 source_run: source.run_index,
4103 start_byte_in_run: source.item_index,
4104 },
4105 affinity: if point.x
4106 < closest_item.position.x + (closest_item.item.bounds().width / 2.0)
4107 {
4108 CursorAffinity::Leading
4109 } else {
4110 CursorAffinity::Trailing
4111 },
4112 });
4113 }
4114 _ => return None,
4115 };
4116
4117 let cluster_mid_x = closest_item.position.x + cluster.advance / 2.0;
4119 let affinity = if point.x < cluster_mid_x {
4120 CursorAffinity::Leading
4121 } else {
4122 CursorAffinity::Trailing
4123 };
4124
4125 Some(TextCursor {
4126 cluster_id: cluster.source_cluster_id,
4127 affinity,
4128 })
4129 }
4130
4131 pub fn get_selection_rects(&self, range: &SelectionRange) -> Vec<LogicalRect> {
4134 let mut cluster_map: HashMap<GraphemeClusterId, &PositionedItem> = HashMap::new();
4136 for item in &self.items {
4137 if let Some(cluster) = item.item.as_cluster() {
4138 cluster_map.insert(cluster.source_cluster_id, item);
4139 }
4140 }
4141
4142 let (start_cursor, end_cursor) = if range.start.cluster_id > range.end.cluster_id
4144 || (range.start.cluster_id == range.end.cluster_id
4145 && range.start.affinity > range.end.affinity)
4146 {
4147 (range.end, range.start)
4148 } else {
4149 (range.start, range.end)
4150 };
4151
4152 let Some(start_item) = cluster_map.get(&start_cursor.cluster_id) else {
4154 return Vec::new();
4155 };
4156 let Some(end_item) = cluster_map.get(&end_cursor.cluster_id) else {
4157 return Vec::new();
4158 };
4159
4160 let mut rects = Vec::new();
4161
4162 let get_cursor_x = |item: &PositionedItem, affinity: CursorAffinity| -> f32 {
4164 match affinity {
4165 CursorAffinity::Leading => item.position.x,
4166 CursorAffinity::Trailing => item.position.x + get_item_measure(&item.item, false),
4167 }
4168 };
4169
4170 let get_line_bounds = |line_index: usize| -> Option<LogicalRect> {
4172 let items_on_line = self.items.iter().filter(|i| i.line_index == line_index);
4173
4174 let mut min_x: Option<f32> = None;
4175 let mut max_x: Option<f32> = None;
4176 let mut min_y: Option<f32> = None;
4177 let mut max_y: Option<f32> = None;
4178
4179 for item in items_on_line {
4180 let item_bounds = item.item.bounds();
4182 if item_bounds.width <= 0.0 && item_bounds.height <= 0.0 {
4183 continue;
4184 }
4185
4186 let item_x_end = item.position.x + item_bounds.width;
4187 let item_y_end = item.position.y + item_bounds.height;
4188
4189 min_x = Some(min_x.map_or(item.position.x, |mx| mx.min(item.position.x)));
4190 max_x = Some(max_x.map_or(item_x_end, |mx| mx.max(item_x_end)));
4191 min_y = Some(min_y.map_or(item.position.y, |my| my.min(item.position.y)));
4192 max_y = Some(max_y.map_or(item_y_end, |my| my.max(item_y_end)));
4193 }
4194
4195 if let (Some(min_x), Some(max_x), Some(min_y), Some(max_y)) =
4196 (min_x, max_x, min_y, max_y)
4197 {
4198 Some(LogicalRect {
4199 origin: LogicalPosition { x: min_x, y: min_y },
4200 size: LogicalSize {
4201 width: max_x - min_x,
4202 height: max_y - min_y,
4203 },
4204 })
4205 } else {
4206 None
4207 }
4208 };
4209
4210 if start_item.line_index == end_item.line_index {
4212 if let Some(line_bounds) = get_line_bounds(start_item.line_index) {
4213 let start_x = get_cursor_x(start_item, start_cursor.affinity);
4214 let end_x = get_cursor_x(end_item, end_cursor.affinity);
4215
4216 rects.push(LogicalRect {
4218 origin: LogicalPosition {
4219 x: start_x.min(end_x),
4220 y: line_bounds.origin.y,
4221 },
4222 size: LogicalSize {
4223 width: (end_x - start_x).abs(),
4224 height: line_bounds.size.height,
4225 },
4226 });
4227 }
4228 }
4229 else {
4231 if let Some(start_line_bounds) = get_line_bounds(start_item.line_index) {
4233 let start_x = get_cursor_x(start_item, start_cursor.affinity);
4234 let line_end_x = start_line_bounds.origin.x + start_line_bounds.size.width;
4235 rects.push(LogicalRect {
4236 origin: LogicalPosition {
4237 x: start_x,
4238 y: start_line_bounds.origin.y,
4239 },
4240 size: LogicalSize {
4241 width: line_end_x - start_x,
4242 height: start_line_bounds.size.height,
4243 },
4244 });
4245 }
4246
4247 for line_idx in (start_item.line_index + 1)..end_item.line_index {
4249 if let Some(line_bounds) = get_line_bounds(line_idx) {
4250 rects.push(line_bounds);
4251 }
4252 }
4253
4254 if let Some(end_line_bounds) = get_line_bounds(end_item.line_index) {
4256 let line_start_x = end_line_bounds.origin.x;
4257 let end_x = get_cursor_x(end_item, end_cursor.affinity);
4258 rects.push(LogicalRect {
4259 origin: LogicalPosition {
4260 x: line_start_x,
4261 y: end_line_bounds.origin.y,
4262 },
4263 size: LogicalSize {
4264 width: end_x - line_start_x,
4265 height: end_line_bounds.size.height,
4266 },
4267 });
4268 }
4269 }
4270
4271 rects
4272 }
4273
4274 pub fn get_cursor_rect(&self, cursor: &TextCursor) -> Option<LogicalRect> {
4276 let mut last_cluster: Option<(&PositionedItem, &ShapedCluster)> = None;
4278 for item in &self.items {
4279 if let ShapedItem::Cluster(cluster) = &item.item {
4280 if cluster.source_cluster_id == cursor.cluster_id {
4281 let line_height = item.item.bounds().height;
4283 let cursor_x = match cursor.affinity {
4284 CursorAffinity::Leading => item.position.x,
4285 CursorAffinity::Trailing => item.position.x + cluster.advance,
4286 };
4287 return Some(LogicalRect {
4288 origin: LogicalPosition {
4289 x: cursor_x,
4290 y: item.position.y,
4291 },
4292 size: LogicalSize {
4293 width: 1.0,
4294 height: line_height,
4295 },
4296 });
4297 }
4298 last_cluster = Some((item, cluster));
4299 }
4300 }
4301 if let Some((item, cluster)) = last_cluster {
4303 if cursor.cluster_id.source_run == cluster.source_cluster_id.source_run
4304 && cursor.cluster_id.start_byte_in_run >= cluster.source_cluster_id.start_byte_in_run
4305 {
4306 let line_height = item.item.bounds().height;
4307 return Some(LogicalRect {
4308 origin: LogicalPosition {
4309 x: item.position.x + cluster.advance,
4310 y: item.position.y,
4311 },
4312 size: LogicalSize {
4313 width: 1.0,
4314 height: line_height,
4315 },
4316 });
4317 }
4318 }
4319 None
4320 }
4321
4322 pub fn get_first_cluster_cursor(&self) -> Option<TextCursor> {
4324 for item in &self.items {
4325 if let ShapedItem::Cluster(cluster) = &item.item {
4326 return Some(TextCursor {
4327 cluster_id: cluster.source_cluster_id,
4328 affinity: CursorAffinity::Leading,
4329 });
4330 }
4331 }
4332 None
4333 }
4334
4335 pub fn get_last_cluster_cursor(&self) -> Option<TextCursor> {
4337 for item in self.items.iter().rev() {
4338 if let ShapedItem::Cluster(cluster) = &item.item {
4339 return Some(TextCursor {
4340 cluster_id: cluster.source_cluster_id,
4341 affinity: CursorAffinity::Trailing,
4342 });
4343 }
4344 }
4345 None
4346 }
4347
4348 pub fn move_cursor_left(
4350 &self,
4351 cursor: TextCursor,
4352 debug: &mut Option<Vec<String>>,
4353 ) -> TextCursor {
4354 if let Some(d) = debug {
4355 d.push(format!(
4356 "[Cursor] move_cursor_left: starting at byte {}, affinity {:?}",
4357 cursor.cluster_id.start_byte_in_run, cursor.affinity
4358 ));
4359 }
4360
4361 let current_item_pos = self.items.iter().position(|i| {
4363 i.item
4364 .as_cluster()
4365 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
4366 });
4367
4368 let Some(current_pos) = current_item_pos else {
4369 if let Some(d) = debug {
4370 d.push(format!(
4371 "[Cursor] move_cursor_left: cursor not found, staying at byte {}",
4372 cursor.cluster_id.start_byte_in_run
4373 ));
4374 }
4375 return cursor;
4376 };
4377
4378 let current_line = self.items[current_pos].line_index;
4384
4385 if let Some(d) = debug {
4386 d.push(format!(
4387 "[Cursor] move_cursor_left: at leading edge, current line {}",
4388 current_line
4389 ));
4390 }
4391
4392 for i in (0..current_pos).rev() {
4394 if let Some(cluster) = self.items[i].item.as_cluster() {
4395 if self.items[i].line_index == current_line {
4396 if let Some(d) = debug {
4397 d.push(format!(
4398 "[Cursor] move_cursor_left: found previous cluster on same line, byte \
4399 {}",
4400 cluster.source_cluster_id.start_byte_in_run
4401 ));
4402 }
4403 return TextCursor {
4404 cluster_id: cluster.source_cluster_id,
4405 affinity: CursorAffinity::Trailing,
4406 };
4407 }
4408 }
4409 }
4410
4411 if current_line > 0 {
4413 let prev_line = current_line - 1;
4414 if let Some(d) = debug {
4415 d.push(format!(
4416 "[Cursor] move_cursor_left: trying previous line {}",
4417 prev_line
4418 ));
4419 }
4420 for i in (0..current_pos).rev() {
4421 if let Some(cluster) = self.items[i].item.as_cluster() {
4422 if self.items[i].line_index == prev_line {
4423 if let Some(d) = debug {
4424 d.push(format!(
4425 "[Cursor] move_cursor_left: found cluster on previous line, byte \
4426 {}",
4427 cluster.source_cluster_id.start_byte_in_run
4428 ));
4429 }
4430 return TextCursor {
4431 cluster_id: cluster.source_cluster_id,
4432 affinity: CursorAffinity::Trailing,
4433 };
4434 }
4435 }
4436 }
4437 }
4438
4439 if let Some(d) = debug {
4441 d.push(format!(
4442 "[Cursor] move_cursor_left: at start of text, staying at byte {}",
4443 cursor.cluster_id.start_byte_in_run
4444 ));
4445 }
4446 cursor
4447 }
4448
4449 pub fn move_cursor_right(
4451 &self,
4452 cursor: TextCursor,
4453 debug: &mut Option<Vec<String>>,
4454 ) -> TextCursor {
4455 if let Some(d) = debug {
4456 d.push(format!(
4457 "[Cursor] move_cursor_right: starting at byte {}, affinity {:?}",
4458 cursor.cluster_id.start_byte_in_run, cursor.affinity
4459 ));
4460 }
4461
4462 let current_item_pos = self.items.iter().position(|i| {
4464 i.item
4465 .as_cluster()
4466 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
4467 });
4468
4469 let Some(current_pos) = current_item_pos else {
4470 if let Some(d) = debug {
4471 d.push(format!(
4472 "[Cursor] move_cursor_right: cursor not found, staying at byte {}",
4473 cursor.cluster_id.start_byte_in_run
4474 ));
4475 }
4476 return cursor;
4477 };
4478
4479 let current_line = self.items[current_pos].line_index;
4487
4488 if let Some(d) = debug {
4489 d.push(format!(
4490 "[Cursor] move_cursor_right: at trailing edge, current line {}",
4491 current_line
4492 ));
4493 }
4494
4495 for i in (current_pos + 1)..self.items.len() {
4497 if let Some(cluster) = self.items[i].item.as_cluster() {
4498 if self.items[i].line_index == current_line {
4499 if let Some(d) = debug {
4500 d.push(format!(
4501 "[Cursor] move_cursor_right: found next cluster on same line, byte {}",
4502 cluster.source_cluster_id.start_byte_in_run
4503 ));
4504 }
4505 return TextCursor {
4506 cluster_id: cluster.source_cluster_id,
4507 affinity: CursorAffinity::Leading,
4508 };
4509 }
4510 }
4511 }
4512
4513 let next_line = current_line + 1;
4515 if let Some(d) = debug {
4516 d.push(format!(
4517 "[Cursor] move_cursor_right: trying next line {}",
4518 next_line
4519 ));
4520 }
4521 for i in (current_pos + 1)..self.items.len() {
4522 if let Some(cluster) = self.items[i].item.as_cluster() {
4523 if self.items[i].line_index == next_line {
4524 if let Some(d) = debug {
4525 d.push(format!(
4526 "[Cursor] move_cursor_right: found cluster on next line, byte {}",
4527 cluster.source_cluster_id.start_byte_in_run
4528 ));
4529 }
4530 return TextCursor {
4531 cluster_id: cluster.source_cluster_id,
4532 affinity: CursorAffinity::Leading,
4533 };
4534 }
4535 }
4536 }
4537
4538 if let Some(d) = debug {
4540 d.push(format!(
4541 "[Cursor] move_cursor_right: at end of text, staying at byte {}",
4542 cursor.cluster_id.start_byte_in_run
4543 ));
4544 }
4545 cursor
4546 }
4547
4548 pub fn move_cursor_up(
4550 &self,
4551 cursor: TextCursor,
4552 goal_x: &mut Option<f32>,
4553 debug: &mut Option<Vec<String>>,
4554 ) -> TextCursor {
4555 if let Some(d) = debug {
4556 d.push(format!(
4557 "[Cursor] move_cursor_up: from byte {} (affinity {:?})",
4558 cursor.cluster_id.start_byte_in_run, cursor.affinity
4559 ));
4560 }
4561
4562 let Some(current_item) = self.items.iter().find(|i| {
4563 i.item
4564 .as_cluster()
4565 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
4566 }) else {
4567 if let Some(d) = debug {
4568 d.push(format!(
4569 "[Cursor] move_cursor_up: cursor not found in items, staying at byte {}",
4570 cursor.cluster_id.start_byte_in_run
4571 ));
4572 }
4573 return cursor;
4574 };
4575
4576 if let Some(d) = debug {
4577 d.push(format!(
4578 "[Cursor] move_cursor_up: current line {}, position ({}, {})",
4579 current_item.line_index, current_item.position.x, current_item.position.y
4580 ));
4581 }
4582
4583 let target_line_idx = current_item.line_index.saturating_sub(1);
4584 if current_item.line_index == target_line_idx {
4585 if let Some(d) = debug {
4586 d.push(format!(
4587 "[Cursor] move_cursor_up: already at top line {}, staying put",
4588 current_item.line_index
4589 ));
4590 }
4591 return cursor;
4592 }
4593
4594 let current_x = goal_x.unwrap_or_else(|| {
4595 let x = match cursor.affinity {
4596 CursorAffinity::Leading => current_item.position.x,
4597 CursorAffinity::Trailing => {
4598 current_item.position.x + get_item_measure(¤t_item.item, false)
4599 }
4600 };
4601 *goal_x = Some(x);
4602 x
4603 });
4604
4605 let target_y = self
4607 .items
4608 .iter()
4609 .find(|i| i.line_index == target_line_idx)
4610 .map(|i| i.position.y + (i.item.bounds().height / 2.0))
4611 .unwrap_or(current_item.position.y);
4612
4613 if let Some(d) = debug {
4614 d.push(format!(
4615 "[Cursor] move_cursor_up: target line {}, hittesting at ({}, {})",
4616 target_line_idx, current_x, target_y
4617 ));
4618 }
4619
4620 let result = self
4621 .hittest_cursor(LogicalPosition {
4622 x: current_x,
4623 y: target_y,
4624 })
4625 .unwrap_or(cursor);
4626
4627 if let Some(d) = debug {
4628 d.push(format!(
4629 "[Cursor] move_cursor_up: result byte {} (affinity {:?})",
4630 result.cluster_id.start_byte_in_run, result.affinity
4631 ));
4632 }
4633
4634 result
4635 }
4636
4637 pub fn move_cursor_down(
4639 &self,
4640 cursor: TextCursor,
4641 goal_x: &mut Option<f32>,
4642 debug: &mut Option<Vec<String>>,
4643 ) -> TextCursor {
4644 if let Some(d) = debug {
4645 d.push(format!(
4646 "[Cursor] move_cursor_down: from byte {} (affinity {:?})",
4647 cursor.cluster_id.start_byte_in_run, cursor.affinity
4648 ));
4649 }
4650
4651 let Some(current_item) = self.items.iter().find(|i| {
4652 i.item
4653 .as_cluster()
4654 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
4655 }) else {
4656 if let Some(d) = debug {
4657 d.push(format!(
4658 "[Cursor] move_cursor_down: cursor not found in items, staying at byte {}",
4659 cursor.cluster_id.start_byte_in_run
4660 ));
4661 }
4662 return cursor;
4663 };
4664
4665 if let Some(d) = debug {
4666 d.push(format!(
4667 "[Cursor] move_cursor_down: current line {}, position ({}, {})",
4668 current_item.line_index, current_item.position.x, current_item.position.y
4669 ));
4670 }
4671
4672 let max_line = self.items.iter().map(|i| i.line_index).max().unwrap_or(0);
4673 let target_line_idx = (current_item.line_index + 1).min(max_line);
4674 if current_item.line_index == target_line_idx {
4675 if let Some(d) = debug {
4676 d.push(format!(
4677 "[Cursor] move_cursor_down: already at bottom line {}, staying put",
4678 current_item.line_index
4679 ));
4680 }
4681 return cursor;
4682 }
4683
4684 let current_x = goal_x.unwrap_or_else(|| {
4685 let x = match cursor.affinity {
4686 CursorAffinity::Leading => current_item.position.x,
4687 CursorAffinity::Trailing => {
4688 current_item.position.x + get_item_measure(¤t_item.item, false)
4689 }
4690 };
4691 *goal_x = Some(x);
4692 x
4693 });
4694
4695 let target_y = self
4696 .items
4697 .iter()
4698 .find(|i| i.line_index == target_line_idx)
4699 .map(|i| i.position.y + (i.item.bounds().height / 2.0))
4700 .unwrap_or(current_item.position.y);
4701
4702 if let Some(d) = debug {
4703 d.push(format!(
4704 "[Cursor] move_cursor_down: hit testing at ({}, {})",
4705 current_x, target_y
4706 ));
4707 }
4708
4709 let result = self
4710 .hittest_cursor(LogicalPosition {
4711 x: current_x,
4712 y: target_y,
4713 })
4714 .unwrap_or(cursor);
4715
4716 if let Some(d) = debug {
4717 d.push(format!(
4718 "[Cursor] move_cursor_down: result byte {}, affinity {:?}",
4719 result.cluster_id.start_byte_in_run, result.affinity
4720 ));
4721 }
4722
4723 result
4724 }
4725
4726 pub fn move_cursor_to_line_start(
4728 &self,
4729 cursor: TextCursor,
4730 debug: &mut Option<Vec<String>>,
4731 ) -> TextCursor {
4732 if let Some(d) = debug {
4733 d.push(format!(
4734 "[Cursor] move_cursor_to_line_start: starting at byte {}, affinity {:?}",
4735 cursor.cluster_id.start_byte_in_run, cursor.affinity
4736 ));
4737 }
4738
4739 let Some(current_item) = self.items.iter().find(|i| {
4740 i.item
4741 .as_cluster()
4742 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
4743 }) else {
4744 if let Some(d) = debug {
4745 d.push(format!(
4746 "[Cursor] move_cursor_to_line_start: cursor not found, staying at byte {}",
4747 cursor.cluster_id.start_byte_in_run
4748 ));
4749 }
4750 return cursor;
4751 };
4752
4753 if let Some(d) = debug {
4754 d.push(format!(
4755 "[Cursor] move_cursor_to_line_start: current line {}, position ({}, {})",
4756 current_item.line_index, current_item.position.x, current_item.position.y
4757 ));
4758 }
4759
4760 let first_item_on_line = self
4761 .items
4762 .iter()
4763 .filter(|i| i.line_index == current_item.line_index)
4764 .min_by(|a, b| {
4765 a.position
4766 .x
4767 .partial_cmp(&b.position.x)
4768 .unwrap_or(Ordering::Equal)
4769 });
4770
4771 if let Some(item) = first_item_on_line {
4772 if let ShapedItem::Cluster(c) = &item.item {
4773 let result = TextCursor {
4774 cluster_id: c.source_cluster_id,
4775 affinity: CursorAffinity::Leading,
4776 };
4777 if let Some(d) = debug {
4778 d.push(format!(
4779 "[Cursor] move_cursor_to_line_start: result byte {}, affinity {:?}",
4780 result.cluster_id.start_byte_in_run, result.affinity
4781 ));
4782 }
4783 return result;
4784 }
4785 }
4786
4787 if let Some(d) = debug {
4788 d.push(format!(
4789 "[Cursor] move_cursor_to_line_start: no first item found, staying at byte {}",
4790 cursor.cluster_id.start_byte_in_run
4791 ));
4792 }
4793 cursor
4794 }
4795
4796 pub fn move_cursor_to_line_end(
4798 &self,
4799 cursor: TextCursor,
4800 debug: &mut Option<Vec<String>>,
4801 ) -> TextCursor {
4802 if let Some(d) = debug {
4803 d.push(format!(
4804 "[Cursor] move_cursor_to_line_end: starting at byte {}, affinity {:?}",
4805 cursor.cluster_id.start_byte_in_run, cursor.affinity
4806 ));
4807 }
4808
4809 let Some(current_item) = self.items.iter().find(|i| {
4810 i.item
4811 .as_cluster()
4812 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
4813 }) else {
4814 if let Some(d) = debug {
4815 d.push(format!(
4816 "[Cursor] move_cursor_to_line_end: cursor not found, staying at byte {}",
4817 cursor.cluster_id.start_byte_in_run
4818 ));
4819 }
4820 return cursor;
4821 };
4822
4823 if let Some(d) = debug {
4824 d.push(format!(
4825 "[Cursor] move_cursor_to_line_end: current line {}, position ({}, {})",
4826 current_item.line_index, current_item.position.x, current_item.position.y
4827 ));
4828 }
4829
4830 let last_item_on_line = self
4831 .items
4832 .iter()
4833 .filter(|i| i.line_index == current_item.line_index)
4834 .max_by(|a, b| {
4835 a.position
4836 .x
4837 .partial_cmp(&b.position.x)
4838 .unwrap_or(Ordering::Equal)
4839 });
4840
4841 if let Some(item) = last_item_on_line {
4842 if let ShapedItem::Cluster(c) = &item.item {
4843 let result = TextCursor {
4844 cluster_id: c.source_cluster_id,
4845 affinity: CursorAffinity::Trailing,
4846 };
4847 if let Some(d) = debug {
4848 d.push(format!(
4849 "[Cursor] move_cursor_to_line_end: result byte {}, affinity {:?}",
4850 result.cluster_id.start_byte_in_run, result.affinity
4851 ));
4852 }
4853 return result;
4854 }
4855 }
4856
4857 if let Some(d) = debug {
4858 d.push(format!(
4859 "[Cursor] move_cursor_to_line_end: no last item found, staying at byte {}",
4860 cursor.cluster_id.start_byte_in_run
4861 ));
4862 }
4863 cursor
4864 }
4865
4866 pub fn move_cursor_to_prev_word(
4872 &self,
4873 cursor: TextCursor,
4874 debug: &mut Option<Vec<String>>,
4875 ) -> TextCursor {
4876 if let Some(d) = debug {
4877 d.push(format!(
4878 "[Cursor] move_cursor_to_prev_word: starting at byte {}, affinity {:?}",
4879 cursor.cluster_id.start_byte_in_run, cursor.affinity
4880 ));
4881 }
4882
4883 let current_pos = match self.items.iter().position(|i| {
4884 i.item
4885 .as_cluster()
4886 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
4887 }) {
4888 Some(pos) => pos,
4889 None => return cursor,
4890 };
4891
4892 let mut pos = if cursor.affinity == CursorAffinity::Leading {
4894 current_pos.checked_sub(1)
4896 } else {
4897 Some(current_pos)
4899 };
4900
4901 while let Some(p) = pos {
4903 if let Some(cluster) = self.items[p].item.as_cluster() {
4904 if !cluster.text.chars().all(|c| c.is_whitespace()) {
4905 break;
4906 }
4907 }
4908 pos = p.checked_sub(1);
4909 }
4910
4911 while let Some(p) = pos {
4913 if let Some(cluster) = self.items[p].item.as_cluster() {
4914 if cluster.text.chars().all(|c| c.is_whitespace()) {
4915 if p + 1 < self.items.len() {
4917 if let Some(c) = self.items[p + 1].item.as_cluster() {
4918 return TextCursor {
4919 cluster_id: c.source_cluster_id,
4920 affinity: CursorAffinity::Leading,
4921 };
4922 }
4923 }
4924 break;
4925 }
4926 }
4927 if p == 0 {
4928 if let Some(c) = self.items[0].item.as_cluster() {
4930 return TextCursor {
4931 cluster_id: c.source_cluster_id,
4932 affinity: CursorAffinity::Leading,
4933 };
4934 }
4935 break;
4936 }
4937 pos = p.checked_sub(1);
4938 }
4939
4940 if pos.is_none() {
4942 if let Some(first) = self.get_first_cluster_cursor() {
4943 return first;
4944 }
4945 }
4946
4947 cursor
4948 }
4949
4950 pub fn move_cursor_to_next_word(
4956 &self,
4957 cursor: TextCursor,
4958 debug: &mut Option<Vec<String>>,
4959 ) -> TextCursor {
4960 if let Some(d) = debug {
4961 d.push(format!(
4962 "[Cursor] move_cursor_to_next_word: starting at byte {}, affinity {:?}",
4963 cursor.cluster_id.start_byte_in_run, cursor.affinity
4964 ));
4965 }
4966
4967 let current_pos = match self.items.iter().position(|i| {
4968 i.item
4969 .as_cluster()
4970 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
4971 }) {
4972 Some(pos) => pos,
4973 None => return cursor,
4974 };
4975
4976 let len = self.items.len();
4977
4978 let start = if cursor.affinity == CursorAffinity::Trailing {
4980 current_pos + 1
4981 } else {
4982 current_pos
4983 };
4984
4985 if start >= len {
4986 return cursor;
4987 }
4988
4989 let mut pos = start;
4990
4991 while pos < len {
4993 if let Some(cluster) = self.items[pos].item.as_cluster() {
4994 if cluster.text.chars().all(|c| c.is_whitespace()) {
4995 break;
4996 }
4997 }
4998 pos += 1;
4999 }
5000
5001 while pos < len {
5003 if let Some(cluster) = self.items[pos].item.as_cluster() {
5004 if !cluster.text.chars().all(|c| c.is_whitespace()) {
5005 return TextCursor {
5007 cluster_id: cluster.source_cluster_id,
5008 affinity: CursorAffinity::Leading,
5009 };
5010 }
5011 }
5012 pos += 1;
5013 }
5014
5015 if let Some(last) = self.get_last_cluster_cursor() {
5017 return last;
5018 }
5019
5020 cursor
5021 }
5022}
5023
5024fn get_baseline_for_item(item: &ShapedItem) -> Option<f32> {
5025 match item {
5026 ShapedItem::CombinedBlock {
5027 baseline_offset, ..
5028 } => Some(*baseline_offset),
5029 ShapedItem::Object {
5030 baseline_offset, ..
5031 } => Some(*baseline_offset),
5032 ShapedItem::Cluster(ref cluster) => {
5034 if let Some(last_glyph) = cluster.glyphs.last() {
5035 Some(
5036 last_glyph
5037 .font_metrics
5038 .baseline_scaled(last_glyph.style.font_size_px),
5039 )
5040 } else {
5041 None
5042 }
5043 }
5044 ShapedItem::Break { source, break_info } => {
5045 None
5047 }
5048 ShapedItem::Tab { source, bounds } => {
5049 None
5051 }
5052 }
5053}
5054
5055#[derive(Debug, Clone, Default)]
5057pub struct OverflowInfo {
5058 pub overflow_items: Vec<ShapedItem>,
5060 pub unclipped_bounds: Rect,
5063}
5064
5065impl OverflowInfo {
5066 pub fn has_overflow(&self) -> bool {
5067 !self.overflow_items.is_empty()
5068 }
5069}
5070
5071#[derive(Debug, Clone)]
5073pub struct UnifiedLine {
5074 pub items: Vec<ShapedItem>,
5075 pub cross_axis_position: f32,
5077 pub constraints: LineConstraints,
5079 pub is_last: bool,
5080}
5081
5082pub type CacheId = u64;
5085
5086#[derive(Debug, Clone)]
5088pub struct LayoutFragment {
5089 pub id: String,
5091 pub constraints: UnifiedConstraints,
5093}
5094
5095#[derive(Debug, Clone)]
5097pub struct FlowLayout {
5098 pub fragment_layouts: HashMap<String, Arc<UnifiedLayout>>,
5100 pub remaining_items: Vec<ShapedItem>,
5103}
5104
5105#[derive(Debug, Clone, Default)]
5115pub struct IntrinsicTextSizes {
5116 pub min_content_width: f32,
5118 pub max_content_width: f32,
5120 pub max_content_height: f32,
5122}
5123
5124#[derive(Clone, Debug)]
5129pub struct CachedLineBreaks {
5130 pub line_ranges: Vec<(usize, usize)>,
5132 pub line_widths: Vec<f32>,
5134 pub available_width: f32,
5136}
5137
5138#[derive(Debug)]
5140pub enum IncrementalRelayoutResult {
5141 GlyphSwap,
5143 LineShift {
5145 affected_item: usize,
5147 delta: f32,
5149 },
5150 PartialReflow {
5152 reflow_from_line: usize,
5154 },
5155 FullRelayout,
5157}
5158
5159pub fn extract_line_breaks(
5161 items: &[PositionedItem],
5162 available_width: f32,
5163) -> CachedLineBreaks {
5164 let mut line_ranges = Vec::new();
5165 let mut line_widths = Vec::new();
5166
5167 if items.is_empty() {
5168 return CachedLineBreaks { line_ranges, line_widths, available_width };
5169 }
5170
5171 let mut line_start = 0usize;
5172 let mut current_line = items[0].line_index;
5173 let mut line_width = 0.0f32;
5174
5175 for (i, item) in items.iter().enumerate() {
5176 if item.line_index != current_line {
5177 line_ranges.push((line_start, i));
5178 line_widths.push(line_width);
5179 line_start = i;
5180 current_line = item.line_index;
5181 line_width = 0.0;
5182 }
5183 line_width += get_item_measure(&item.item, false);
5184 }
5185
5186 line_ranges.push((line_start, items.len()));
5188 line_widths.push(line_width);
5189
5190 CachedLineBreaks { line_ranges, line_widths, available_width }
5191}
5192
5193pub fn try_incremental_relayout(
5200 dirty_item_indices: &[usize],
5201 old_advances: &[f32],
5202 new_advances: &[f32],
5203 line_breaks: &CachedLineBreaks,
5204) -> IncrementalRelayoutResult {
5205 if dirty_item_indices.is_empty() {
5206 return IncrementalRelayoutResult::GlyphSwap;
5207 }
5208
5209 for &dirty_idx in dirty_item_indices {
5211 if dirty_idx >= old_advances.len() || dirty_idx >= new_advances.len() {
5212 return IncrementalRelayoutResult::FullRelayout;
5213 }
5214
5215 let old_adv = old_advances[dirty_idx];
5216 let new_adv = new_advances[dirty_idx];
5217 let delta = new_adv - old_adv;
5218
5219 if delta.abs() < 0.001 {
5220 continue;
5222 }
5223
5224 let line_idx = line_breaks.line_ranges.iter()
5226 .position(|&(start, end)| dirty_idx >= start && dirty_idx < end);
5227
5228 let Some(line_idx) = line_idx else {
5229 return IncrementalRelayoutResult::FullRelayout;
5230 };
5231
5232 let old_line_width = line_breaks.line_widths[line_idx];
5233 let new_line_width = old_line_width + delta;
5234
5235 if new_line_width <= line_breaks.available_width {
5236 return IncrementalRelayoutResult::LineShift {
5238 affected_item: dirty_idx,
5239 delta,
5240 };
5241 } else {
5242 return IncrementalRelayoutResult::PartialReflow {
5244 reflow_from_line: line_idx as usize,
5245 };
5246 }
5247 }
5248
5249 IncrementalRelayoutResult::GlyphSwap
5251}
5252
5253pub struct PerItemShapedEntry {
5256 pub clusters: Vec<ShapedItem>,
5258 pub total_advance: f32,
5260}
5261
5262pub struct TextShapingCache {
5263 logical_items: HashMap<CacheId, Arc<Vec<LogicalItem>>>,
5265 visual_items: HashMap<CacheId, Arc<Vec<VisualItem>>>,
5267 shaped_items: HashMap<CacheId, Arc<Vec<ShapedItem>>>,
5269 per_item_shaped: HashMap<u64, Arc<PerItemShapedEntry>>,
5272 per_item_accessed: HashSet<u64>,
5274 generation: u64,
5276}
5277
5278#[derive(Debug, Clone, Default)]
5280pub struct TextCacheMemoryReport {
5281 pub logical_items_entries: usize,
5282 pub logical_items_bytes: usize,
5283 pub visual_items_entries: usize,
5284 pub visual_items_bytes: usize,
5285 pub shaped_items_entries: usize,
5286 pub shaped_items_bytes: usize,
5287 pub shaped_glyph_bytes: usize,
5288 pub shaped_cluster_text_bytes: usize,
5289 pub per_item_shaped_entries: usize,
5290 pub per_item_shaped_bytes: usize,
5291}
5292
5293impl TextCacheMemoryReport {
5294 pub fn total_bytes(&self) -> usize {
5295 self.logical_items_bytes
5296 + self.visual_items_bytes
5297 + self.shaped_items_bytes
5298 + self.shaped_glyph_bytes
5299 + self.shaped_cluster_text_bytes
5300 + self.per_item_shaped_bytes
5301 }
5302}
5303
5304impl TextShapingCache {
5305 pub fn new() -> Self {
5306 Self {
5307 logical_items: HashMap::new(),
5308 visual_items: HashMap::new(),
5309 shaped_items: HashMap::new(),
5310 per_item_shaped: HashMap::new(),
5311 per_item_accessed: HashSet::new(),
5312 generation: 0,
5313 }
5314 }
5315
5316 pub fn memory_report(&self) -> TextCacheMemoryReport {
5318 let mut r = TextCacheMemoryReport::default();
5319 r.logical_items_entries = self.logical_items.len();
5320 for (_, arc) in &self.logical_items {
5321 r.logical_items_bytes += arc.capacity() * core::mem::size_of::<LogicalItem>();
5322 }
5323 r.visual_items_entries = self.visual_items.len();
5324 for (_, arc) in &self.visual_items {
5325 r.visual_items_bytes += arc.capacity() * core::mem::size_of::<VisualItem>();
5326 }
5327 r.shaped_items_entries = self.shaped_items.len();
5328 for (_, arc) in &self.shaped_items {
5329 r.shaped_items_bytes += arc.capacity() * core::mem::size_of::<ShapedItem>();
5330 for item in arc.iter() {
5331 if let ShapedItem::Cluster(c) = item {
5332 r.shaped_glyph_bytes += c.glyphs.capacity() * core::mem::size_of::<ShapedGlyph>();
5333 r.shaped_cluster_text_bytes += c.text.capacity();
5334 }
5335 }
5336 }
5337 r.per_item_shaped_entries = self.per_item_shaped.len();
5338 for (_, arc) in &self.per_item_shaped {
5339 r.per_item_shaped_bytes += arc.clusters.capacity() * core::mem::size_of::<ShapedItem>();
5340 for item in arc.clusters.iter() {
5341 if let ShapedItem::Cluster(c) = item {
5342 r.per_item_shaped_bytes += c.glyphs.capacity() * core::mem::size_of::<ShapedGlyph>();
5343 r.per_item_shaped_bytes += c.text.capacity();
5344 }
5345 }
5346 }
5347 r
5348 }
5349
5350 pub fn begin_generation(&mut self) {
5353 if self.generation > 0 && !self.per_item_accessed.is_empty() {
5354 let accessed = &self.per_item_accessed;
5356 self.per_item_shaped.retain(|k, _| accessed.contains(k));
5357 }
5358 self.per_item_accessed.clear();
5359 self.generation += 1;
5360 }
5361
5362 pub fn use_old_layout(
5377 old_constraints: &UnifiedConstraints,
5378 new_constraints: &UnifiedConstraints,
5379 old_content: &[InlineContent],
5380 new_content: &[InlineContent],
5381 ) -> bool {
5382 if old_constraints != new_constraints {
5384 return false;
5385 }
5386
5387 if old_content.len() != new_content.len() {
5389 return false;
5390 }
5391
5392 for (old, new) in old_content.iter().zip(new_content.iter()) {
5394 if !Self::inline_content_layout_eq(old, new) {
5395 return false;
5396 }
5397 }
5398
5399 true
5400 }
5401
5402 fn inline_content_layout_eq(old: &InlineContent, new: &InlineContent) -> bool {
5406 use InlineContent::*;
5407 match (old, new) {
5408 (Text(old_run), Text(new_run)) => {
5409 old_run.text == new_run.text
5411 && old_run.style.layout_eq(&new_run.style)
5412 }
5413 (Image(old_img), Image(new_img)) => {
5414 old_img.intrinsic_size == new_img.intrinsic_size
5416 && old_img.display_size == new_img.display_size
5417 && old_img.baseline_offset == new_img.baseline_offset
5418 && old_img.alignment == new_img.alignment
5419 }
5420 (Space(old_sp), Space(new_sp)) => old_sp == new_sp,
5421 (LineBreak(old_br), LineBreak(new_br)) => old_br == new_br,
5422 (Tab { style: old_style }, Tab { style: new_style }) => old_style.layout_eq(new_style),
5423 (Marker { run: old_run, position_outside: old_pos },
5424 Marker { run: new_run, position_outside: new_pos }) => {
5425 old_pos == new_pos
5426 && old_run.text == new_run.text
5427 && old_run.style.layout_eq(&new_run.style)
5428 }
5429 (Shape(old_shape), Shape(new_shape)) => {
5430 old_shape.shape_def == new_shape.shape_def
5432 && old_shape.baseline_offset == new_shape.baseline_offset
5433 }
5434 (Ruby { base: old_base, text: old_text, style: old_style },
5435 Ruby { base: new_base, text: new_text, style: new_style }) => {
5436 old_style.layout_eq(new_style)
5437 && old_base.len() == new_base.len()
5438 && old_text.len() == new_text.len()
5439 && old_base.iter().zip(new_base.iter())
5440 .all(|(o, n)| Self::inline_content_layout_eq(o, n))
5441 && old_text.iter().zip(new_text.iter())
5442 .all(|(o, n)| Self::inline_content_layout_eq(o, n))
5443 }
5444 _ => false,
5446 }
5447 }
5448}
5449
5450impl Default for TextShapingCache {
5451 fn default() -> Self {
5452 Self::new()
5453 }
5454}
5455
5456#[derive(Debug, Clone, Eq, PartialEq, Hash)]
5458pub struct LogicalItemsKey<'a> {
5459 pub inline_content_hash: u64, pub default_font_size: u32, pub _marker: std::marker::PhantomData<&'a ()>,
5463}
5464
5465#[derive(Debug, Clone, Eq, PartialEq, Hash)]
5467pub struct VisualItemsKey {
5468 pub logical_items_id: CacheId,
5469 pub base_direction: BidiDirection,
5470}
5471
5472#[derive(Debug, Clone, Eq, PartialEq, Hash)]
5474pub struct ShapedItemsKey {
5475 pub visual_items_id: CacheId,
5476 pub style_hash: u64, }
5478
5479impl ShapedItemsKey {
5480 pub fn new(visual_items_id: CacheId, visual_items: &[VisualItem]) -> Self {
5481 let style_hash = {
5482 let mut hasher = DefaultHasher::new();
5483 for item in visual_items.iter() {
5484 match &item.logical_source {
5486 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
5487 style.as_ref().hash(&mut hasher);
5488 }
5489 _ => {}
5490 }
5491 }
5492 hasher.finish()
5493 };
5494
5495 Self {
5496 visual_items_id,
5497 style_hash,
5498 }
5499 }
5500}
5501
5502#[derive(Debug, Clone, Eq, PartialEq, Hash)]
5504pub struct LayoutKey {
5505 pub shaped_items_id: CacheId,
5506 pub constraints: UnifiedConstraints,
5507}
5508
5509fn calculate_id<T: Hash>(item: &T) -> CacheId {
5511 let mut hasher = DefaultHasher::new();
5512 item.hash(&mut hasher);
5513 hasher.finish()
5514}
5515
5516impl TextShapingCache {
5519 pub fn layout_flow<T: ParsedFontTrait>(
5577 &mut self,
5578 content: &[InlineContent],
5579 style_overrides: &[StyleOverride],
5580 flow_chain: &[LayoutFragment],
5581 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
5582 fc_cache: &FcFontCache,
5583 loaded_fonts: &LoadedFonts<T>,
5584 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
5585 ) -> Result<FlowLayout, LayoutError> {
5586 const PER_ITEM_CACHE_MAX: usize = 4096;
5593 if self.per_item_shaped.len() > PER_ITEM_CACHE_MAX {
5594 self.begin_generation();
5595 }
5596
5597 let logical_items_id = calculate_id(&content);
5599 let logical_items = self
5600 .logical_items
5601 .entry(logical_items_id)
5602 .or_insert_with(|| {
5603 Arc::new(create_logical_items(
5604 content,
5605 style_overrides,
5606 debug_messages,
5607 ))
5608 })
5609 .clone();
5610
5611 let default_constraints = UnifiedConstraints::default();
5614 let first_constraints = flow_chain
5615 .first()
5616 .map(|f| &f.constraints)
5617 .unwrap_or(&default_constraints);
5618
5619 let unicode_bidi_val = first_constraints.unicode_bidi;
5631 let base_direction = if unicode_bidi_val == UnicodeBidi::Plaintext {
5632 let has_strong = logical_items.iter().any(|item| {
5634 if let LogicalItem::Text { text, .. } = item {
5635 matches!(unicode_bidi::get_base_direction(text.as_str()),
5636 unicode_bidi::Direction::Ltr | unicode_bidi::Direction::Rtl)
5637 } else {
5638 false
5639 }
5640 });
5641 if has_strong {
5642 get_base_direction_from_logical(&logical_items)
5643 } else {
5644 first_constraints.direction.unwrap_or(BidiDirection::Ltr)
5646 }
5647 } else {
5648 first_constraints.direction.unwrap_or(BidiDirection::Ltr)
5650 };
5651 let visual_key = VisualItemsKey {
5652 logical_items_id,
5653 base_direction,
5654 };
5655 let visual_items_id = calculate_id(&visual_key);
5656 let visual_items = self
5657 .visual_items
5658 .entry(visual_items_id)
5659 .or_insert_with(|| {
5660 Arc::new(
5661 reorder_logical_items(&logical_items, base_direction, unicode_bidi_val, debug_messages).unwrap(),
5662 )
5663 })
5664 .clone();
5665
5666 let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
5669 let shaped_items_id = calculate_id(&shaped_key);
5670 let shaped_items = match self.shaped_items.get(&shaped_items_id) {
5671 Some(cached) => {
5672 cached.clone()
5674 }
5675 None => {
5676 let items = Arc::new(shape_visual_items_with_per_item_cache(
5679 &visual_items,
5680 &mut self.per_item_shaped,
5681 &mut self.per_item_accessed,
5682 font_chain_cache,
5683 fc_cache,
5684 loaded_fonts,
5685 debug_messages,
5686 )?);
5687 self.shaped_items.insert(shaped_items_id, items.clone());
5688 items
5689 }
5690 };
5691
5692 let oriented_items = apply_text_orientation(shaped_items, first_constraints)?;
5699
5700 let mut fragment_layouts = HashMap::new();
5702 let mut cursor = BreakCursor::with_word_break(&oriented_items, first_constraints.word_break);
5705 cursor.hyphens = first_constraints.hyphenation;
5706 cursor.line_break = first_constraints.line_break;
5707
5708 for fragment in flow_chain {
5709 let fragment_layout = perform_fragment_layout(
5711 &mut cursor,
5712 &logical_items,
5713 &fragment.constraints,
5714 debug_messages,
5715 loaded_fonts,
5716 )?;
5717
5718 fragment_layouts.insert(fragment.id.clone(), Arc::new(fragment_layout));
5719 if cursor.is_done() {
5720 break; }
5722 }
5723
5724 Ok(FlowLayout {
5725 fragment_layouts,
5726 remaining_items: cursor.drain_remaining(),
5727 })
5728 }
5729
5730 pub fn measure_intrinsic_widths<T: ParsedFontTrait>(
5746 &mut self,
5747 content: &[InlineContent],
5748 style_overrides: &[StyleOverride],
5749 constraints: &UnifiedConstraints,
5750 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
5751 fc_cache: &FcFontCache,
5752 loaded_fonts: &LoadedFonts<T>,
5753 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
5754 ) -> Result<IntrinsicTextSizes, LayoutError> {
5755 const PER_ITEM_CACHE_MAX: usize = 4096;
5756 if self.per_item_shaped.len() > PER_ITEM_CACHE_MAX {
5757 self.begin_generation();
5758 }
5759
5760 let logical_items_id = calculate_id(&content);
5762 let logical_items = self
5763 .logical_items
5764 .entry(logical_items_id)
5765 .or_insert_with(|| {
5766 Arc::new(create_logical_items(
5767 content,
5768 style_overrides,
5769 debug_messages,
5770 ))
5771 })
5772 .clone();
5773
5774 let unicode_bidi_val = constraints.unicode_bidi;
5776 let base_direction = if unicode_bidi_val == UnicodeBidi::Plaintext {
5777 let has_strong = logical_items.iter().any(|item| {
5778 if let LogicalItem::Text { text, .. } = item {
5779 matches!(unicode_bidi::get_base_direction(text.as_str()),
5780 unicode_bidi::Direction::Ltr | unicode_bidi::Direction::Rtl)
5781 } else {
5782 false
5783 }
5784 });
5785 if has_strong {
5786 get_base_direction_from_logical(&logical_items)
5787 } else {
5788 constraints.direction.unwrap_or(BidiDirection::Ltr)
5789 }
5790 } else {
5791 constraints.direction.unwrap_or(BidiDirection::Ltr)
5792 };
5793 let visual_key = VisualItemsKey {
5794 logical_items_id,
5795 base_direction,
5796 };
5797 let visual_items_id = calculate_id(&visual_key);
5798 let visual_items = self
5799 .visual_items
5800 .entry(visual_items_id)
5801 .or_insert_with(|| {
5802 Arc::new(
5803 reorder_logical_items(&logical_items, base_direction, unicode_bidi_val, debug_messages).unwrap(),
5804 )
5805 })
5806 .clone();
5807
5808 let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
5810 let shaped_items_id = calculate_id(&shaped_key);
5811 let shaped_items = match self.shaped_items.get(&shaped_items_id) {
5812 Some(cached) => cached.clone(),
5813 None => {
5814 let items = Arc::new(shape_visual_items_with_per_item_cache(
5815 &visual_items,
5816 &mut self.per_item_shaped,
5817 &mut self.per_item_accessed,
5818 font_chain_cache,
5819 fc_cache,
5820 loaded_fonts,
5821 debug_messages,
5822 )?);
5823 self.shaped_items.insert(shaped_items_id, items.clone());
5824 items
5825 }
5826 };
5827
5828 let oriented_items = apply_text_orientation(shaped_items, constraints)?;
5830
5831 let word_break = constraints.word_break;
5833 let hyphens = constraints.hyphenation;
5834
5835 let mut total = 0.0f32;
5836 let mut max_word = 0.0f32;
5837 let mut cur_word = 0.0f32;
5838 let mut max_line_height = 0.0f32;
5839
5840 for item in oriented_items.iter() {
5841 let advance = match item {
5842 ShapedItem::Cluster(c) => c.advance,
5843 ShapedItem::CombinedBlock { bounds, .. }
5844 | ShapedItem::Object { bounds, .. }
5845 | ShapedItem::Tab { bounds, .. } => bounds.width,
5846 ShapedItem::Break { .. } => 0.0,
5847 };
5848 let adv = advance.max(0.0);
5849 total += adv;
5850
5851 let (asc, desc) = get_item_vertical_metrics_approx(item);
5852 let h = (asc + desc).max(item.bounds().height);
5853 if h > max_line_height {
5854 max_line_height = h;
5855 }
5856
5857 if is_break_opportunity_with_word_break(item, word_break, hyphens) {
5858 if cur_word > max_word {
5859 max_word = cur_word;
5860 }
5861 cur_word = 0.0;
5862 } else {
5863 cur_word += adv;
5864 }
5865 }
5866 if cur_word > max_word {
5867 max_word = cur_word;
5868 }
5869
5870 Ok(IntrinsicTextSizes {
5871 min_content_width: max_word,
5872 max_content_width: total,
5873 max_content_height: max_line_height,
5874 })
5875 }
5876}
5877
5878pub fn create_logical_items(
5880 content: &[InlineContent],
5881 style_overrides: &[StyleOverride],
5882 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
5883) -> Vec<LogicalItem> {
5884 if let Some(msgs) = debug_messages {
5885 msgs.push(LayoutDebugMessage::info(
5886 "\n--- Entering create_logical_items (Refactored) ---".to_string(),
5887 ));
5888 msgs.push(LayoutDebugMessage::info(format!(
5889 "Input content length: {}",
5890 content.len()
5891 )));
5892 msgs.push(LayoutDebugMessage::info(format!(
5893 "Input overrides length: {}",
5894 style_overrides.len()
5895 )));
5896 }
5897
5898 let mut items = Vec::new();
5899 let mut style_cache: HashMap<u64, Arc<StyleProperties>> = HashMap::new();
5900
5901 let mut run_overrides: HashMap<u32, HashMap<u32, &PartialStyleProperties>> = HashMap::new();
5903 for override_item in style_overrides {
5904 run_overrides
5905 .entry(override_item.target.run_index)
5906 .or_default()
5907 .insert(override_item.target.item_index, &override_item.style);
5908 }
5909
5910 for (run_idx, inline_item) in content.iter().enumerate() {
5911 if let Some(msgs) = debug_messages {
5912 msgs.push(LayoutDebugMessage::info(format!(
5913 "Processing content run #{}",
5914 run_idx
5915 )));
5916 }
5917
5918 let marker_position_outside = match inline_item {
5920 InlineContent::Marker {
5921 position_outside, ..
5922 } => Some(*position_outside),
5923 _ => None,
5924 };
5925
5926 match inline_item {
5927 InlineContent::Text(run) | InlineContent::Marker { run, .. } => {
5928 let text = &run.text;
5929 if text.is_empty() {
5930 if let Some(msgs) = debug_messages {
5931 msgs.push(LayoutDebugMessage::info(
5932 " Run is empty, skipping.".to_string(),
5933 ));
5934 }
5935 continue;
5936 }
5937 if let Some(msgs) = debug_messages {
5938 msgs.push(LayoutDebugMessage::info(format!(" Run text: '{}'", text)));
5939 }
5940
5941 let current_run_overrides = run_overrides.get(&(run_idx as u32));
5942 let mut boundaries = BTreeSet::new();
5943 boundaries.insert(0);
5944 boundaries.insert(text.len());
5945
5946 let mut scan_cursor = 0;
5948 while scan_cursor < text.len() {
5949 let style_at_cursor = if let Some(partial) =
5950 current_run_overrides.and_then(|o| o.get(&(scan_cursor as u32)))
5951 {
5952 run.style.apply_override(partial)
5954 } else {
5955 (*run.style).clone()
5956 };
5957
5958 let current_char = text[scan_cursor..].chars().next().unwrap();
5959
5960 if let Some(TextCombineUpright::Digits(max_digits)) =
5965 style_at_cursor.text_combine_upright
5966 {
5967 if max_digits > 0 && current_char.is_ascii_digit() {
5968 let digit_chunk: String = text[scan_cursor..]
5969 .chars()
5970 .take(max_digits as usize)
5971 .take_while(|c| c.is_ascii_digit())
5972 .collect();
5973
5974 let end_of_chunk = scan_cursor + digit_chunk.len();
5975 boundaries.insert(scan_cursor);
5976 boundaries.insert(end_of_chunk);
5977 scan_cursor = end_of_chunk; continue;
5979 }
5980 }
5981
5982 if current_run_overrides
5985 .and_then(|o| o.get(&(scan_cursor as u32)))
5986 .is_some()
5987 {
5988 let grapheme_len = text[scan_cursor..]
5989 .graphemes(true)
5990 .next()
5991 .unwrap_or("")
5992 .len();
5993 boundaries.insert(scan_cursor);
5994 boundaries.insert(scan_cursor + grapheme_len);
5995 scan_cursor += grapheme_len;
5996 continue;
5997 }
5998
5999 scan_cursor += current_char.len_utf8();
6002 }
6003
6004 if let Some(msgs) = debug_messages {
6005 msgs.push(LayoutDebugMessage::info(format!(
6006 " Boundaries: {:?}",
6007 boundaries
6008 )));
6009 }
6010
6011 for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
6013 let (start, end) = (*start, *end);
6014 if start >= end {
6015 continue;
6016 }
6017
6018 let text_slice = &text[start..end];
6019 if let Some(msgs) = debug_messages {
6020 msgs.push(LayoutDebugMessage::info(format!(
6021 " Processing chunk from {} to {}: '{}'",
6022 start, end, text_slice
6023 )));
6024 }
6025
6026 let style_to_use = if let Some(partial_style) =
6027 current_run_overrides.and_then(|o| o.get(&(start as u32)))
6028 {
6029 if let Some(msgs) = debug_messages {
6030 msgs.push(LayoutDebugMessage::info(format!(
6031 " -> Applying override at byte {}",
6032 start
6033 )));
6034 }
6035 let mut hasher = DefaultHasher::new();
6036 Arc::as_ptr(&run.style).hash(&mut hasher);
6037 partial_style.hash(&mut hasher);
6038 style_cache
6039 .entry(hasher.finish())
6040 .or_insert_with(|| Arc::new(run.style.apply_override(partial_style)))
6041 .clone()
6042 } else {
6043 run.style.clone()
6044 };
6045
6046 let is_combinable_chunk = match &style_to_use.text_combine_upright {
6053 Some(TextCombineUpright::All) => !text_slice.is_empty(),
6054 Some(TextCombineUpright::Digits(max_digits)) => {
6055 *max_digits > 0
6056 && !text_slice.is_empty()
6057 && text_slice.chars().all(|c| c.is_ascii_digit())
6058 && text_slice.chars().count() <= *max_digits as usize
6059 }
6060 _ => false,
6061 };
6062
6063 if is_combinable_chunk {
6064 let trimmed = text_slice.trim();
6066 let combined_text = if trimmed.is_empty() {
6067 text_slice.to_string()
6068 } else {
6069 trimmed.to_string()
6070 };
6071 items.push(LogicalItem::CombinedText {
6072 source: ContentIndex {
6073 run_index: run_idx as u32,
6074 item_index: start as u32,
6075 },
6076 text: combined_text,
6077 style: style_to_use,
6078 });
6079 } else {
6080 items.push(LogicalItem::Text {
6081 source: ContentIndex {
6082 run_index: run_idx as u32,
6083 item_index: start as u32,
6084 },
6085 text: text_slice.to_string(),
6086 style: style_to_use,
6087 marker_position_outside,
6088 source_node_id: run.source_node_id,
6089 });
6090 }
6091 }
6092 }
6093 InlineContent::LineBreak(break_info) => {
6095 if let Some(msgs) = debug_messages {
6096 msgs.push(LayoutDebugMessage::info(format!(
6097 " LineBreak: {:?}",
6098 break_info
6099 )));
6100 }
6101 items.push(LogicalItem::Break {
6102 source: ContentIndex {
6103 run_index: run_idx as u32,
6104 item_index: 0,
6105 },
6106 break_info: break_info.clone(),
6107 });
6108 }
6109 InlineContent::Tab { style } => {
6111 if let Some(msgs) = debug_messages {
6112 msgs.push(LayoutDebugMessage::info(" Tab character".to_string()));
6113 }
6114 items.push(LogicalItem::Tab {
6115 source: ContentIndex {
6116 run_index: run_idx as u32,
6117 item_index: 0,
6118 },
6119 style: style.clone(),
6120 });
6121 }
6122 _ => {
6124 if let Some(msgs) = debug_messages {
6125 msgs.push(LayoutDebugMessage::info(
6126 " Run is not text, creating generic LogicalItem.".to_string(),
6127 ));
6128 }
6129 items.push(LogicalItem::Object {
6130 source: ContentIndex {
6131 run_index: run_idx as u32,
6132 item_index: 0,
6133 },
6134 content: inline_item.clone(),
6135 });
6136 }
6137 }
6138 }
6139 if let Some(msgs) = debug_messages {
6140 msgs.push(LayoutDebugMessage::info(format!(
6141 "--- Exiting create_logical_items, created {} items ---",
6142 items.len()
6143 )));
6144 }
6145 items
6146}
6147
6148pub fn get_base_direction_from_logical(logical_items: &[LogicalItem]) -> BidiDirection {
6154 let first_strong = logical_items.iter().find_map(|item| {
6155 if let LogicalItem::Text { text, .. } = item {
6156 Some(unicode_bidi::get_base_direction(text.as_str()))
6157 } else {
6158 None
6159 }
6160 });
6161
6162 match first_strong {
6163 Some(unicode_bidi::Direction::Rtl) => BidiDirection::Rtl,
6164 _ => BidiDirection::Ltr,
6165 }
6166}
6167
6168pub fn reorder_logical_items(
6180 logical_items: &[LogicalItem],
6181 base_direction: BidiDirection,
6182 unicode_bidi: UnicodeBidi,
6183 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6184) -> Result<Vec<VisualItem>, LayoutError> {
6185 if let Some(msgs) = debug_messages {
6186 msgs.push(LayoutDebugMessage::info(
6187 "\n--- Entering reorder_logical_items ---".to_string(),
6188 ));
6189 msgs.push(LayoutDebugMessage::info(format!(
6190 "Input logical items count: {}",
6191 logical_items.len()
6192 )));
6193 msgs.push(LayoutDebugMessage::info(format!(
6194 "Base direction: {:?}",
6195 base_direction
6196 )));
6197 }
6198
6199 let mut bidi_str = String::new();
6201 let mut item_map = Vec::new();
6202 for (idx, item) in logical_items.iter().enumerate() {
6203 let text = match item {
6222 LogicalItem::Text { text, .. } => text.as_str(),
6223 LogicalItem::CombinedText { text, .. } => text.as_str(),
6224 _ => "\u{FFFC}",
6225 };
6226 let start_byte = bidi_str.len();
6227 bidi_str.push_str(text);
6228 for _ in start_byte..bidi_str.len() {
6229 item_map.push(idx);
6230 }
6231 }
6232
6233 if bidi_str.is_empty() {
6234 if let Some(msgs) = debug_messages {
6235 msgs.push(LayoutDebugMessage::info(
6236 "Bidi string is empty, returning.".to_string(),
6237 ));
6238 }
6239 return Ok(Vec::new());
6240 }
6241 if let Some(msgs) = debug_messages {
6242 msgs.push(LayoutDebugMessage::info(format!(
6243 "Constructed bidi string: '{}'",
6244 bidi_str
6245 )));
6246 }
6247
6248 let bidi_level = if unicode_bidi == UnicodeBidi::Plaintext {
6253 None
6254 } else if base_direction == BidiDirection::Rtl {
6255 Some(Level::rtl())
6256 } else {
6257 Some(Level::ltr())
6258 };
6259 let bidi_info = BidiInfo::new(&bidi_str, bidi_level);
6261 let para = &bidi_info.paragraphs[0];
6262 let (levels, visual_runs) = bidi_info.visual_runs(para, para.range.clone());
6263
6264 if let Some(msgs) = debug_messages {
6265 msgs.push(LayoutDebugMessage::info(
6266 "Bidi visual runs generated:".to_string(),
6267 ));
6268 for (i, run_range) in visual_runs.iter().enumerate() {
6269 let level = levels[run_range.start].number();
6270 let slice = &bidi_str[run_range.start..run_range.end];
6271 msgs.push(LayoutDebugMessage::info(format!(
6272 " Run {}: range={:?}, level={}, text='{}'",
6273 i, run_range, level, slice
6274 )));
6275 }
6276 }
6277
6278 let mut visual_items = Vec::new();
6279 for run_range in visual_runs {
6280 let bidi_level = BidiLevel::new(levels[run_range.start].number());
6281 let mut sub_run_start = run_range.start;
6282
6283 for i in (run_range.start + 1)..run_range.end {
6284 if item_map[i] != item_map[sub_run_start] {
6285 let logical_idx = item_map[sub_run_start];
6286 let logical_item = &logical_items[logical_idx];
6287 let text_slice = &bidi_str[sub_run_start..i];
6288 visual_items.push(VisualItem {
6289 logical_source: logical_item.clone(),
6290 bidi_level,
6291 script: crate::text3::script::detect_script(text_slice)
6292 .unwrap_or(Script::Latin),
6293 text: text_slice.to_string(),
6294 });
6295 sub_run_start = i;
6296 }
6297 }
6298
6299 let logical_idx = item_map[sub_run_start];
6300 let logical_item = &logical_items[logical_idx];
6301 let text_slice = &bidi_str[sub_run_start..run_range.end];
6302 visual_items.push(VisualItem {
6303 logical_source: logical_item.clone(),
6304 bidi_level,
6305 script: crate::text3::script::detect_script(text_slice).unwrap_or(Script::Latin),
6306 text: text_slice.to_string(),
6307 });
6308 }
6309
6310 if let Some(msgs) = debug_messages {
6311 msgs.push(LayoutDebugMessage::info(
6312 "Final visual items produced:".to_string(),
6313 ));
6314 for (i, item) in visual_items.iter().enumerate() {
6315 msgs.push(LayoutDebugMessage::info(format!(
6316 " Item {}: level={}, text='{}'",
6317 i,
6318 item.bidi_level.level(),
6319 item.text
6320 )));
6321 }
6322 msgs.push(LayoutDebugMessage::info(
6323 "--- Exiting reorder_logical_items ---".to_string(),
6324 ));
6325 }
6326 Ok(visual_items)
6327}
6328
6329pub fn shape_visual_items_with_per_item_cache<T: ParsedFontTrait>(
6354 visual_items: &[VisualItem],
6355 per_item_cache: &mut HashMap<u64, Arc<PerItemShapedEntry>>,
6356 per_item_accessed: &mut HashSet<u64>,
6357 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
6358 fc_cache: &FcFontCache,
6359 loaded_fonts: &LoadedFonts<T>,
6360 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6361) -> Result<Vec<ShapedItem>, LayoutError> {
6362 let mut shaped = Vec::new();
6369 let mut idx = 0;
6370
6371 while idx < visual_items.len() {
6372 let item = &visual_items[idx];
6373
6374 let (layout_hash, bidi_level, script) = match &item.logical_source {
6376 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
6377 (style.layout_hash(), item.bidi_level, item.script)
6378 }
6379 _ => {
6380 let single = shape_visual_items(
6382 &visual_items[idx..idx+1],
6383 font_chain_cache, fc_cache, loaded_fonts, debug_messages,
6384 )?;
6385 shaped.extend(single);
6386 idx += 1;
6387 continue;
6388 }
6389 };
6390
6391 let mut coalesce_end = idx + 1;
6392 while coalesce_end < visual_items.len() {
6393 let next = &visual_items[coalesce_end];
6394 let next_layout_hash = match &next.logical_source {
6395 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
6396 Some(style.layout_hash())
6397 }
6398 _ => None,
6399 };
6400 if let Some(nlh) = next_layout_hash {
6401 if nlh == layout_hash
6402 && next.bidi_level == bidi_level
6403 && next.script == script
6404 {
6405 coalesce_end += 1;
6406 } else {
6407 break;
6408 }
6409 } else {
6410 break;
6411 }
6412 }
6413
6414 let mut hasher = std::collections::hash_map::DefaultHasher::new();
6416 use std::hash::{Hash, Hasher};
6417 for j in idx..coalesce_end {
6418 visual_items[j].text.hash(&mut hasher);
6419 }
6420 layout_hash.hash(&mut hasher);
6421 bidi_level.hash(&mut hasher);
6422 (script as u32).hash(&mut hasher);
6423 let group_key = hasher.finish();
6424
6425 per_item_accessed.insert(group_key);
6427 if let Some(cached) = per_item_cache.get(&group_key) {
6428 shaped.extend(cached.clusters.iter().cloned());
6429 } else {
6430 let group_items = shape_visual_items(
6432 &visual_items[idx..coalesce_end],
6433 font_chain_cache, fc_cache, loaded_fonts, debug_messages,
6434 )?;
6435 let total_advance: f32 = group_items.iter().map(|item| {
6436 match item {
6437 ShapedItem::Cluster(c) => c.advance,
6438 _ => 0.0,
6439 }
6440 }).sum();
6441 per_item_cache.insert(group_key, Arc::new(PerItemShapedEntry {
6442 clusters: group_items.clone(),
6443 total_advance,
6444 }));
6445 shaped.extend(group_items);
6446 }
6447
6448 idx = coalesce_end;
6449 }
6450
6451 Ok(shaped)
6452}
6453
6454fn split_text_by_font_coverage(
6459 text: &str,
6460 font_chain: &rust_fontconfig::FontFallbackChain,
6461 fc_cache: &FcFontCache,
6462) -> Vec<(usize, usize, FontId)> {
6463 let mut segments: Vec<(usize, usize, FontId)> = Vec::new();
6464
6465 for (byte_idx, ch) in text.char_indices() {
6466 let char_end = byte_idx + ch.len_utf8();
6467 if let Some((font_id, _)) = font_chain.resolve_char(fc_cache, ch) {
6468 match segments.last_mut() {
6469 Some(last) if last.2 == font_id && last.1 == byte_idx => {
6470 last.1 = char_end;
6472 }
6473 _ => {
6474 segments.push((byte_idx, char_end, font_id));
6476 }
6477 }
6478 }
6479 }
6480
6481 segments
6482}
6483
6484fn shape_with_font_fallback<T: ParsedFontTrait>(
6490 text: &str,
6491 script: Script,
6492 language: crate::text3::script::Language,
6493 direction: BidiDirection,
6494 style: &Arc<StyleProperties>,
6495 source_index: ContentIndex,
6496 source_node_id: Option<NodeId>,
6497 font_chain: &rust_fontconfig::FontFallbackChain,
6498 fc_cache: &FcFontCache,
6499 loaded_fonts: &LoadedFonts<T>,
6500) -> Result<Vec<ShapedCluster>, LayoutError> {
6501 static FONT_FB_DEBUG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6509 let dbg = *FONT_FB_DEBUG.get_or_init(|| {
6510 std::env::var_os("AZ_FONT_FALLBACK_DEBUG").is_some()
6511 });
6512
6513 let segments = split_text_by_font_coverage(text, font_chain, fc_cache);
6514
6515 if dbg && segments.len() > 1 {
6516 eprintln!(
6517 "[FONT FALLBACK] text needs {} font segments for '{}' ({}..{} bytes)",
6518 segments.len(),
6519 text.chars().take(40).collect::<String>(),
6520 0, text.len()
6521 );
6522 }
6523
6524 if segments.len() <= 1 {
6525 let (seg_start, seg_end, font_id) = match segments.first() {
6527 Some(s) => s,
6528 None => {
6529 if dbg {
6530 eprintln!("[FONT FALLBACK] no font could render any char in '{}'", text.chars().take(20).collect::<String>());
6531 }
6532 return Ok(Vec::new());
6533 }
6534 };
6535 let font = match loaded_fonts.get(font_id) {
6536 Some(f) => f,
6537 None => {
6538 if dbg {
6539 eprintln!("[FONT FALLBACK] font {:?} not in loaded_fonts for '{}'", font_id, text.chars().take(20).collect::<String>());
6540 }
6541 return Ok(Vec::new());
6542 }
6543 };
6544 if *seg_start == 0 && *seg_end == text.len() {
6546 return shape_text_correctly(
6547 text, script, language, direction,
6548 font, style, source_index, source_node_id,
6549 );
6550 }
6551 let mut clusters = shape_text_correctly(
6552 &text[*seg_start..*seg_end], script, language, direction,
6553 font, style, source_index, source_node_id,
6554 )?;
6555 if *seg_start > 0 {
6556 for cluster in &mut clusters {
6557 cluster.source_cluster_id.start_byte_in_run += *seg_start as u32;
6558 }
6559 }
6560 return Ok(clusters);
6561 }
6562
6563 let mut all_clusters = Vec::new();
6565 for (seg_start, seg_end, font_id) in &segments {
6566 let font = match loaded_fonts.get(font_id) {
6567 Some(f) => f,
6568 None => {
6569 if dbg {
6570 eprintln!("[FONT FALLBACK] font {:?} NOT loaded, skipping segment bytes {}..{}", font_id, seg_start, seg_end);
6571 }
6572 continue;
6573 }
6574 };
6575 let segment_text = &text[*seg_start..*seg_end];
6576 if dbg {
6577 eprintln!(
6578 "[FONT FALLBACK] text='{}' uses font {:?} (bytes {}..{})",
6579 segment_text, font_id, seg_start, seg_end
6580 );
6581 }
6582 let mut seg_clusters = shape_text_correctly(
6583 segment_text, script, language, direction,
6584 font, style, source_index, source_node_id,
6585 )?;
6586 if *seg_start > 0 {
6589 for cluster in &mut seg_clusters {
6590 cluster.source_cluster_id.start_byte_in_run += *seg_start as u32;
6591 }
6592 }
6593 all_clusters.extend(seg_clusters);
6594 }
6595 Ok(all_clusters)
6596}
6597
6598pub fn shape_visual_items<T: ParsedFontTrait>(
6599 visual_items: &[VisualItem],
6600 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
6601 fc_cache: &FcFontCache,
6602 loaded_fonts: &LoadedFonts<T>,
6603 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6604) -> Result<Vec<ShapedItem>, LayoutError> {
6605 let mut shaped = Vec::new();
6606 let mut idx = 0;
6607 let mut _coalesced_runs = 0usize;
6608 let mut _total_runs = 0usize;
6609 let mut _shape_calls = 0usize;
6610
6611 while idx < visual_items.len() {
6614 let item = &visual_items[idx];
6615 match &item.logical_source {
6616 LogicalItem::Text {
6617 style,
6618 source,
6619 marker_position_outside,
6620 source_node_id,
6621 ..
6622 } => {
6623 let layout_hash = style.layout_hash();
6624 let bidi_level = item.bidi_level;
6625 let script = item.script;
6626
6627 let mut coalesce_end = idx + 1;
6633 while coalesce_end < visual_items.len() {
6634 let next = &visual_items[coalesce_end];
6635 if let LogicalItem::Text { style: next_style, .. } = &next.logical_source {
6636 if next_style.layout_hash() == layout_hash
6637 && next.bidi_level == bidi_level
6638 && next.script == script
6639 {
6640 coalesce_end += 1;
6641 } else {
6642 break;
6643 }
6644 } else {
6645 break;
6646 }
6647 }
6648
6649 let coalesce_count = coalesce_end - idx;
6650
6651 if coalesce_count > 1 {
6652 _coalesced_runs += coalesce_count;
6653 _shape_calls += 1;
6654 let total_text_len: usize = visual_items[idx..coalesce_end]
6660 .iter()
6661 .map(|v| v.text.len())
6662 .sum();
6663 let mut merged_text = String::with_capacity(total_text_len);
6664 let mut byte_ranges: Vec<(
6666 usize, usize,
6667 Arc<StyleProperties>,
6668 ContentIndex,
6669 Option<NodeId>,
6670 Option<bool>,
6671 )> = Vec::with_capacity(coalesce_count);
6672
6673 for j in idx..coalesce_end {
6674 let start = merged_text.len();
6675 merged_text.push_str(&visual_items[j].text);
6676 let end = merged_text.len();
6677 if let LogicalItem::Text {
6678 style: s, source: src, source_node_id: nid,
6679 marker_position_outside: mpo, ..
6680 } = &visual_items[j].logical_source {
6681 byte_ranges.push((start, end, s.clone(), *src, *nid, *mpo));
6682 }
6683 }
6684
6685 if let Some(msgs) = debug_messages {
6686 msgs.push(LayoutDebugMessage::info(format!(
6687 "[TextLayout] Coalescing {} text runs ({} bytes) into single shaping call",
6688 coalesce_count, merged_text.len()
6689 )));
6690 }
6691
6692 let direction = if bidi_level.is_rtl() {
6693 BidiDirection::Rtl
6694 } else {
6695 BidiDirection::Ltr
6696 };
6697 let language = script_to_language(script, &merged_text);
6698
6699 let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
6702 FontStack::Ref(font_ref) => {
6703 shape_text_correctly(
6704 &merged_text, script, language, direction,
6705 font_ref, style, *source, *source_node_id,
6706 )
6707 }
6708 FontStack::Stack(selectors) => {
6709 let cache_key = FontChainKey::from_selectors(selectors);
6710 let font_chain = match font_chain_cache.get(&cache_key) {
6711 Some(chain) => chain,
6712 None => { idx = coalesce_end; continue; }
6713 };
6714 shape_with_font_fallback(
6716 &merged_text, script, language, direction,
6717 style, *source, *source_node_id,
6718 font_chain, fc_cache, loaded_fonts,
6719 )
6720 }
6721 };
6722
6723 let shaped_clusters = shaped_clusters_result?;
6724
6725 for cluster in shaped_clusters {
6730 let byte_pos = cluster.source_cluster_id.start_byte_in_run as usize;
6731 let orig = byte_ranges.iter().find(|(start, end, ..)| {
6733 byte_pos >= *start && byte_pos < *end
6734 });
6735 let mut cluster = cluster;
6736 if let Some((range_start, _, orig_style, orig_source, orig_nid, orig_mpo)) = orig {
6737 cluster.style = orig_style.clone();
6739 cluster.source_content_index = *orig_source;
6740 cluster.source_node_id = *orig_nid;
6741 cluster.source_cluster_id.source_run = orig_source.run_index;
6743 cluster.source_cluster_id.start_byte_in_run = (byte_pos - range_start) as u32;
6744 for glyph in &mut cluster.glyphs {
6746 glyph.style = orig_style.clone();
6747 }
6748 if let Some(is_outside) = orig_mpo {
6749 cluster.marker_position_outside = Some(*is_outside);
6750 }
6751 }
6752 shaped.push(ShapedItem::Cluster(cluster));
6753 }
6754
6755 idx = coalesce_end;
6756 continue;
6757 }
6758
6759 _total_runs += 1;
6761 _shape_calls += 1;
6762 let direction = if item.bidi_level.is_rtl() {
6763 BidiDirection::Rtl
6764 } else {
6765 BidiDirection::Ltr
6766 };
6767
6768 let language = script_to_language(item.script, &item.text);
6769
6770 let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
6772 FontStack::Ref(font_ref) => {
6773 if let Some(msgs) = debug_messages {
6775 msgs.push(LayoutDebugMessage::info(format!(
6776 "[TextLayout] Using direct FontRef for text: '{}'",
6777 item.text.chars().take(30).collect::<String>()
6778 )));
6779 }
6780 shape_text_correctly(
6781 &item.text,
6782 item.script,
6783 language,
6784 direction,
6785 font_ref,
6786 style,
6787 *source,
6788 *source_node_id,
6789 )
6790 }
6791 FontStack::Stack(selectors) => {
6792 let cache_key = FontChainKey::from_selectors(selectors);
6794
6795 let font_chain = match font_chain_cache.get(&cache_key) {
6797 Some(chain) => chain,
6798 None => {
6799 if let Some(msgs) = debug_messages {
6800 msgs.push(LayoutDebugMessage::warning(format!(
6801 "[TextLayout] Font chain not pre-resolved for {:?} - text will \
6802 not be rendered",
6803 cache_key.font_families
6804 )));
6805 }
6806 idx += 1;
6807 continue;
6808 }
6809 };
6810
6811 shape_with_font_fallback(
6813 &item.text, item.script, language, direction,
6814 style, *source, *source_node_id,
6815 font_chain, fc_cache, loaded_fonts,
6816 )
6817 }
6818 };
6819
6820 let mut shaped_clusters = shaped_clusters_result?;
6821
6822 if let Some(is_outside) = marker_position_outside {
6824 for cluster in &mut shaped_clusters {
6825 cluster.marker_position_outside = Some(*is_outside);
6826 }
6827 }
6828
6829 shaped.extend(shaped_clusters.into_iter().map(ShapedItem::Cluster));
6830 }
6831 LogicalItem::Tab { source, style } => {
6838 if style.tab_size == 0.0 {
6839 shaped.push(ShapedItem::Tab {
6841 source: *source,
6842 bounds: Rect {
6843 x: 0.0,
6844 y: 0.0,
6845 width: 0.0,
6846 height: 0.0,
6847 },
6848 });
6849 } else {
6850 let space_advance_approx = style.font_size_px * 0.5;
6854 let ls = match style.letter_spacing {
6856 Spacing::Px(px) => px as f32,
6857 Spacing::Em(em) => em * style.font_size_px,
6858 };
6859 let ws = match style.word_spacing {
6860 Spacing::Px(px) => px as f32,
6861 Spacing::Em(em) => em * style.font_size_px,
6862 };
6863 let tab_interval = style.tab_size * (space_advance_approx + ls + ws);
6865 let current_advance: f32 = shaped.iter().map(|item| {
6867 match item {
6868 ShapedItem::Cluster(c) => c.advance,
6869 ShapedItem::Tab { bounds, .. } => bounds.width,
6870 ShapedItem::Object { bounds, .. } => bounds.width,
6871 _ => 0.0,
6872 }
6873 }).sum();
6874 let next_tab_stop = ((current_advance / tab_interval).floor() + 1.0) * tab_interval;
6876 let mut tab_width = next_tab_stop - current_advance;
6877 let half_ch = space_advance_approx * 0.5;
6879 if tab_width < half_ch {
6880 tab_width += tab_interval;
6881 }
6882 shaped.push(ShapedItem::Tab {
6883 source: *source,
6884 bounds: Rect {
6885 x: 0.0,
6886 y: 0.0,
6887 width: tab_width,
6888 height: 0.0,
6889 },
6890 });
6891 }
6892 }
6893 LogicalItem::Ruby {
6894 source,
6895 base_text,
6896 ruby_text,
6897 style,
6898 } => {
6899 let placeholder_width = base_text.chars().count() as f32 * style.font_size_px * 0.6;
6900 shaped.push(ShapedItem::Object {
6901 source: *source,
6902 bounds: Rect {
6903 x: 0.0,
6904 y: 0.0,
6905 width: placeholder_width,
6906 height: style.line_height.resolve(style.font_size_px, 0.0, 0.0, 0.0, 0) * 1.5,
6907 },
6908 baseline_offset: 0.0,
6909 content: InlineContent::Text(StyledRun {
6910 text: base_text.clone(),
6911 style: style.clone(),
6912 logical_start_byte: 0,
6913 source_node_id: None,
6914 }),
6915 });
6916 }
6917 LogicalItem::CombinedText {
6918 style,
6919 source,
6920 text,
6921 } => {
6922 let language = script_to_language(item.script, &item.text);
6923
6924 let text = if text.chars().count() > 1 {
6930 let converted: String = text.chars().map(|c| {
6931 let cp = c as u32;
6932 if cp >= 0xFF01 && cp <= 0xFF5E {
6933 char::from_u32(cp - 0xFF01 + 0x0021).unwrap_or(c)
6935 } else {
6936 c
6937 }
6938 }).collect();
6939 converted
6940 } else {
6941 text.clone()
6942 };
6943
6944 let glyphs: Vec<Glyph> = match &style.font_stack {
6949 FontStack::Ref(font_ref) => {
6950 if let Some(msgs) = debug_messages {
6952 msgs.push(LayoutDebugMessage::info(format!(
6953 "[TextLayout] Using direct FontRef for CombinedText: '{}'",
6954 text.chars().take(30).collect::<String>()
6955 )));
6956 }
6957 font_ref.shape_text(
6958 &text,
6959 item.script,
6960 language,
6961 BidiDirection::Ltr,
6962 style.as_ref(),
6963 )?
6964 }
6965 FontStack::Stack(selectors) => {
6966 let cache_key = FontChainKey::from_selectors(selectors);
6968
6969 let font_chain = match font_chain_cache.get(&cache_key) {
6970 Some(chain) => chain,
6971 None => {
6972 if let Some(msgs) = debug_messages {
6973 msgs.push(LayoutDebugMessage::warning(format!(
6974 "[TextLayout] Font chain not pre-resolved for CombinedText {:?}",
6975 cache_key.font_families
6976 )));
6977 }
6978 idx += 1;
6979 continue;
6980 }
6981 };
6982
6983 let segments = split_text_by_font_coverage(&text, font_chain, fc_cache);
6985 let mut all_glyphs = Vec::new();
6986 for (seg_start, seg_end, font_id) in &segments {
6987 let font = match loaded_fonts.get(font_id) {
6988 Some(f) => f,
6989 None => continue,
6990 };
6991 let segment_text = &text[*seg_start..*seg_end];
6992 let mut seg_glyphs = font.shape_text(
6993 segment_text,
6994 item.script,
6995 language,
6996 BidiDirection::Ltr,
6997 style.as_ref(),
6998 )?;
6999 if *seg_start > 0 {
7001 for g in &mut seg_glyphs {
7002 g.logical_byte_index += *seg_start;
7003 g.cluster += *seg_start as u32;
7004 }
7005 }
7006 all_glyphs.extend(seg_glyphs);
7007 }
7008 if all_glyphs.is_empty() {
7009 idx += 1;
7010 continue;
7011 }
7012 all_glyphs
7013 }
7014 };
7015
7016 let shaped_glyphs: ShapedGlyphVec = glyphs
7017 .into_iter()
7018 .map(|g| ShapedGlyph {
7019 kind: GlyphKind::Character,
7020 glyph_id: g.glyph_id,
7021 script: g.script,
7022 font_hash: g.font_hash,
7023 font_metrics: g.font_metrics,
7024 style: g.style,
7025 cluster_offset: 0,
7026 advance: g.advance,
7027 kerning: g.kerning,
7028 offset: g.offset,
7029 vertical_advance: g.vertical_advance,
7030 vertical_offset: g.vertical_bearing,
7031 })
7032 .collect();
7033
7034 let total_width: f32 = shaped_glyphs.iter().map(|g| g.advance + g.kerning).sum();
7036 let em_size = shaped_glyphs.first()
7042 .map(|g| g.style.font_size_px)
7043 .unwrap_or(style.font_size_px);
7044 let bounds = Rect {
7045 x: 0.0,
7046 y: 0.0,
7047 width: total_width,
7048 height: em_size,
7049 };
7050
7051 shaped.push(ShapedItem::CombinedBlock {
7052 source: *source,
7053 glyphs: shaped_glyphs,
7054 bounds,
7055 baseline_offset: em_size / 2.0,
7056 });
7057 }
7058 LogicalItem::Object {
7059 content, source, ..
7060 } => {
7061 let (bounds, baseline) = measure_inline_object(content)?;
7062 shaped.push(ShapedItem::Object {
7063 source: *source,
7064 bounds,
7065 baseline_offset: baseline,
7066 content: content.clone(),
7067 });
7068 }
7069 LogicalItem::Break { source, break_info } => {
7070 shaped.push(ShapedItem::Break {
7071 source: *source,
7072 break_info: break_info.clone(),
7073 });
7074 }
7075 }
7076 idx += 1;
7077 }
7078
7079 Ok(shaped)
7080}
7081
7082fn is_hanging_punctuation_char(c: char) -> bool {
7085 matches!(c,
7086 ',' | '.' | '\u{060C}' | '\u{06D4}' | '\u{3001}' | '\u{3002}' | '\u{FF0C}' | '\u{FF0E}' | '\u{FE50}' | '\u{FE51}' | '\u{FE52}' | '\u{FF61}' | '\u{FF64}' )
7100}
7101
7102fn is_hanging_punctuation(item: &ShapedItem) -> bool {
7107 if let ShapedItem::Cluster(c) = item {
7108 if c.glyphs.len() == 1 {
7109 c.text.chars().next().map_or(false, is_hanging_punctuation_char)
7110 } else {
7111 false
7112 }
7113 } else {
7114 false
7115 }
7116}
7117
7118fn shape_text_correctly<T: ParsedFontTrait>(
7119 text: &str,
7120 script: Script,
7121 language: crate::text3::script::Language,
7122 direction: BidiDirection,
7123 font: &T, style: &Arc<StyleProperties>,
7125 source_index: ContentIndex,
7126 source_node_id: Option<NodeId>,
7127) -> Result<Vec<ShapedCluster>, LayoutError> {
7128 let glyphs = font.shape_text(text, script, language, direction, style.as_ref())?;
7129
7130 if glyphs.is_empty() {
7131 return Ok(Vec::new());
7132 }
7133
7134 let mut clusters = Vec::new();
7135
7136 let mut current_cluster_glyphs = Vec::new();
7138 let mut cluster_id = glyphs[0].cluster;
7139 let mut cluster_start_byte_in_text = glyphs[0].logical_byte_index;
7140
7141 for glyph in glyphs {
7142 if glyph.cluster != cluster_id {
7143 let advance = current_cluster_glyphs
7145 .iter()
7146 .map(|g: &Glyph| g.advance)
7147 .sum();
7148
7149 let (start, end) = if cluster_start_byte_in_text <= glyph.logical_byte_index {
7152 (cluster_start_byte_in_text, glyph.logical_byte_index)
7153 } else {
7154 (glyph.logical_byte_index, cluster_start_byte_in_text)
7155 };
7156 let cluster_text = text.get(start..end).unwrap_or("");
7157
7158 clusters.push(ShapedCluster {
7159 text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
7161 source_run: source_index.run_index,
7162 start_byte_in_run: cluster_id,
7163 },
7164 source_content_index: source_index,
7165 source_node_id,
7166 glyphs: current_cluster_glyphs
7167 .iter()
7168 .map(|g| {
7169 let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
7171 (g.logical_byte_index - cluster_start_byte_in_text) as u32
7172 } else {
7173 0
7174 };
7175 ShapedGlyph {
7176 kind: if g.glyph_id == 0 {
7177 GlyphKind::NotDef
7178 } else {
7179 GlyphKind::Character
7180 },
7181 glyph_id: g.glyph_id,
7182 script: g.script,
7183 font_hash: g.font_hash,
7184 font_metrics: g.font_metrics.clone(),
7185 style: g.style.clone(),
7186 cluster_offset,
7187 advance: g.advance,
7188 kerning: g.kerning,
7189 vertical_advance: g.vertical_advance,
7190 vertical_offset: g.vertical_bearing,
7191 offset: g.offset,
7192 }
7193 })
7194 .collect(),
7195 advance,
7196 direction,
7197 style: style.clone(),
7198 marker_position_outside: None,
7199 is_first_fragment: true,
7200 is_last_fragment: true,
7201 });
7202 current_cluster_glyphs.clear();
7203 cluster_id = glyph.cluster;
7204 cluster_start_byte_in_text = glyph.logical_byte_index;
7205 }
7206 current_cluster_glyphs.push(glyph);
7207 }
7208
7209 if !current_cluster_glyphs.is_empty() {
7211 let advance = current_cluster_glyphs
7212 .iter()
7213 .map(|g: &Glyph| g.advance)
7214 .sum();
7215 let cluster_text = text.get(cluster_start_byte_in_text..).unwrap_or("");
7216 clusters.push(ShapedCluster {
7217 text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
7219 source_run: source_index.run_index,
7220 start_byte_in_run: cluster_id,
7221 },
7222 source_content_index: source_index,
7223 source_node_id,
7224 glyphs: current_cluster_glyphs
7225 .iter()
7226 .map(|g| {
7227 let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
7229 (g.logical_byte_index - cluster_start_byte_in_text) as u32
7230 } else {
7231 0
7232 };
7233 ShapedGlyph {
7234 kind: if g.glyph_id == 0 {
7235 GlyphKind::NotDef
7236 } else {
7237 GlyphKind::Character
7238 },
7239 glyph_id: g.glyph_id,
7240 font_hash: g.font_hash,
7241 font_metrics: g.font_metrics.clone(),
7242 style: g.style.clone(),
7243 script: g.script,
7244 vertical_advance: g.vertical_advance,
7245 vertical_offset: g.vertical_bearing,
7246 cluster_offset,
7247 advance: g.advance,
7248 kerning: g.kerning,
7249 offset: g.offset,
7250 }
7251 })
7252 .collect(),
7253 advance,
7254 direction,
7255 style: style.clone(),
7256 marker_position_outside: None,
7257 is_first_fragment: true,
7258 is_last_fragment: true,
7259 });
7260 }
7261
7262 Ok(clusters)
7263}
7264
7265fn measure_inline_object(item: &InlineContent) -> Result<(Rect, f32), LayoutError> {
7267 match item {
7268 InlineContent::Image(img) => {
7269 let size = img.display_size.unwrap_or(img.intrinsic_size);
7270 Ok((
7271 Rect {
7272 x: 0.0,
7273 y: 0.0,
7274 width: size.width,
7275 height: size.height,
7276 },
7277 img.baseline_offset,
7278 ))
7279 }
7280 InlineContent::Shape(shape) => Ok({
7281 let size = shape.shape_def.get_size();
7282 (
7283 Rect {
7284 x: 0.0,
7285 y: 0.0,
7286 width: size.width,
7287 height: size.height,
7288 },
7289 shape.baseline_offset,
7290 )
7291 }),
7292 InlineContent::Space(space) => Ok((
7293 Rect {
7294 x: 0.0,
7295 y: 0.0,
7296 width: space.width,
7297 height: 0.0,
7298 },
7299 0.0,
7300 )),
7301 InlineContent::Marker { .. } => {
7302 Err(LayoutError::InvalidText(
7304 "Marker is text content, not a measurable object".into(),
7305 ))
7306 }
7307 _ => Err(LayoutError::InvalidText("Not a measurable object".into())),
7308 }
7309}
7310
7311fn apply_text_orientation(
7317 items: Arc<Vec<ShapedItem>>,
7318 constraints: &UnifiedConstraints,
7319) -> Result<Arc<Vec<ShapedItem>>, LayoutError> {
7320 if !constraints.is_vertical() {
7321 return Ok(items);
7322 }
7323
7324 let mut oriented_items = Vec::with_capacity(items.len());
7325 let writing_mode = constraints.writing_mode.unwrap_or_default();
7326
7327 for item in items.iter() {
7328 match item {
7329 ShapedItem::Cluster(cluster) => {
7330 let mut new_cluster = cluster.clone();
7331 let mut total_vertical_advance = 0.0;
7332
7333 for glyph in &mut new_cluster.glyphs {
7334 if glyph.vertical_advance > 0.0 {
7337 total_vertical_advance += glyph.vertical_advance;
7338 } else {
7339 let fallback_advance = cluster.style.line_height.resolve_with_metrics(cluster.style.font_size_px, &glyph.font_metrics);
7341 glyph.vertical_advance = fallback_advance;
7342 glyph.vertical_offset = Point {
7344 x: -glyph.advance / 2.0,
7345 y: 0.0,
7346 };
7347 total_vertical_advance += fallback_advance;
7348 }
7349 }
7350 new_cluster.advance = total_vertical_advance;
7352 oriented_items.push(ShapedItem::Cluster(new_cluster));
7353 }
7354 ShapedItem::Object {
7356 source,
7357 bounds,
7358 baseline_offset,
7359 content,
7360 } => {
7361 let mut new_bounds = *bounds;
7362 std::mem::swap(&mut new_bounds.width, &mut new_bounds.height);
7363 oriented_items.push(ShapedItem::Object {
7364 source: *source,
7365 bounds: new_bounds,
7366 baseline_offset: *baseline_offset,
7367 content: content.clone(),
7368 });
7369 }
7370 _ => oriented_items.push(item.clone()),
7371 }
7372 }
7373
7374 Ok(Arc::new(oriented_items))
7375}
7376
7377fn get_item_vertical_align(item: &ShapedItem) -> Option<VerticalAlign> {
7386 match item {
7387 ShapedItem::Object { content, .. } => match content {
7388 InlineContent::Image(img) => Some(img.alignment),
7389 InlineContent::Shape(shape) => Some(shape.alignment),
7390 _ => None,
7391 },
7392 _ => None,
7393 }
7394}
7395
7396pub fn get_item_vertical_metrics_approx(item: &ShapedItem) -> (f32, f32) {
7399 if let ShapedItem::Cluster(c) = item {
7401 if !c.glyphs.is_empty() {
7402 let (asc, desc) = c.glyphs
7404 .iter()
7405 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
7406 let metrics = &glyph.font_metrics;
7407 if metrics.units_per_em == 0 {
7408 return (max_asc, max_desc);
7409 }
7410 let scale = glyph.style.font_size_px / metrics.units_per_em as f32;
7411 let font_ascent = metrics.ascent * scale;
7412 let font_descent = (-metrics.descent * scale).max(0.0);
7413 let ad = font_ascent + font_descent;
7414 let resolved_lh = c.style.line_height.resolve_with_metrics(glyph.style.font_size_px, &glyph.font_metrics);
7415 let half_leading = (resolved_lh - ad) / 2.0;
7416 (max_asc.max(font_ascent + half_leading), max_desc.max(font_descent + half_leading))
7417 });
7418 return (asc, desc);
7419 }
7420 }
7421 match item {
7423 ShapedItem::Cluster(c) => {
7424 let lh = c.style.line_height.resolve(c.style.font_size_px, 0.0, 0.0, 0.0, 0);
7425 (lh * 0.8, lh * 0.2)
7426 }
7427 ShapedItem::CombinedBlock { bounds, .. } => (bounds.height * 0.8, bounds.height * 0.2),
7428 ShapedItem::Object { bounds, .. } => (bounds.height, 0.0),
7429 ShapedItem::Tab { bounds, .. } => (bounds.height * 0.8, bounds.height * 0.2),
7430 ShapedItem::Break { .. } => (0.0, 0.0),
7431 }
7432}
7433
7434pub fn get_item_vertical_metrics(item: &ShapedItem, constraints: &UnifiedConstraints) -> (f32, f32) {
7447 match item {
7449 ShapedItem::Cluster(c) => {
7450 if c.glyphs.is_empty() {
7451 let ad = constraints.strut_ascent + constraints.strut_descent;
7457 let resolved_lh = c.style.line_height.resolve(c.style.font_size_px, 0.0, 0.0, 0.0, 0);
7458 let half_leading = (resolved_lh - ad) / 2.0;
7459 return (constraints.strut_ascent + half_leading, constraints.strut_descent + half_leading);
7460 }
7461 c.glyphs
7474 .iter()
7475 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
7476 let metrics = &glyph.font_metrics;
7477 if metrics.units_per_em == 0 {
7478 return (max_asc, max_desc);
7479 }
7480 let scale = glyph.style.font_size_px / metrics.units_per_em as f32;
7481 let a = metrics.ascent * scale;
7482 let d = (-metrics.descent * scale).max(0.0);
7485 let ad = a + d;
7486 let resolved_lh = glyph.style.line_height.resolve_with_metrics(glyph.style.font_size_px, &glyph.font_metrics);
7487 let leading = resolved_lh - ad;
7488 let half_leading = leading / 2.0;
7489 let item_asc = a + half_leading;
7490 let item_desc = d + half_leading;
7491 (max_asc.max(item_asc), max_desc.max(item_desc))
7492 })
7493 }
7494 ShapedItem::Object {
7495 bounds,
7496 baseline_offset,
7497 ..
7498 } => {
7499 let ascent = bounds.height - *baseline_offset;
7502 let descent = *baseline_offset;
7503 (ascent.max(0.0), descent.max(0.0))
7504 }
7505 ShapedItem::CombinedBlock {
7506 bounds,
7507 baseline_offset,
7508 ..
7509 } => {
7510 let ascent = bounds.height - *baseline_offset;
7512 let descent = *baseline_offset;
7513 (ascent.max(0.0), descent.max(0.0))
7514 }
7515 _ => (0.0, 0.0), }
7517}
7518
7519fn calculate_line_metrics(
7533 items: &[ShapedItem],
7534 default_vertical_align: VerticalAlign,
7535 constraints: &UnifiedConstraints,
7536) -> (f32, f32) {
7537 let (mut max_asc, mut max_desc) = items
7541 .iter()
7542 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), item| {
7543 let effective_align = get_item_vertical_align(item)
7544 .unwrap_or(default_vertical_align);
7545 match effective_align {
7546 VerticalAlign::Top | VerticalAlign::Bottom => {
7547 (max_asc, max_desc)
7549 }
7550 _ => {
7551 let (item_asc, item_desc) = get_item_vertical_metrics(item, constraints);
7552 (max_asc.max(item_asc), max_desc.max(item_desc))
7553 }
7554 }
7555 });
7556
7557 let baseline_line_height = max_asc + max_desc;
7558
7559 for item in items {
7562 let effective_align = get_item_vertical_align(item)
7563 .unwrap_or(default_vertical_align);
7564 match effective_align {
7565 VerticalAlign::Top | VerticalAlign::Bottom => {
7566 let (item_asc, item_desc) = get_item_vertical_metrics(item, constraints);
7567 let item_height = item_asc + item_desc;
7568 if item_height > baseline_line_height {
7569 if effective_align == VerticalAlign::Top {
7571 max_desc = max_desc.max(item_height - max_asc);
7573 } else {
7574 max_asc = max_asc.max(item_height - max_desc);
7576 }
7577 }
7578 }
7579 _ => {} }
7581 }
7582
7583 (max_asc, max_desc)
7584}
7585
7586pub fn perform_fragment_layout<T: ParsedFontTrait>(
7623 cursor: &mut BreakCursor,
7624 logical_items: &[LogicalItem],
7625 fragment_constraints: &UnifiedConstraints,
7626 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
7627 fonts: &LoadedFonts<T>,
7628) -> Result<UnifiedLayout, LayoutError> {
7629 if let Some(msgs) = debug_messages {
7630 msgs.push(LayoutDebugMessage::info(
7631 "\n--- Entering perform_fragment_layout ---".to_string(),
7632 ));
7633 msgs.push(LayoutDebugMessage::info(format!(
7634 "Constraints: available_width={:?}, available_height={:?}, columns={}, text_wrap={:?}",
7635 fragment_constraints.available_width,
7636 fragment_constraints.available_height,
7637 fragment_constraints.columns,
7638 fragment_constraints.text_wrap
7639 )));
7640 }
7641
7642 if fragment_constraints.text_wrap == TextWrap::Balance {
7645 if let Some(msgs) = debug_messages {
7646 msgs.push(LayoutDebugMessage::info(
7647 "Using Knuth-Plass algorithm for text-wrap: balance".to_string(),
7648 ));
7649 }
7650
7651 let shaped_items: Vec<ShapedItem> = cursor.drain_remaining();
7653
7654 let hyphenator = if fragment_constraints.hyphenation == Hyphens::Auto {
7656 fragment_constraints
7657 .hyphenation_language
7658 .and_then(|lang| get_hyphenator(lang).ok())
7659 } else {
7660 None
7661 };
7662
7663 return crate::text3::knuth_plass::kp_layout(
7665 &shaped_items,
7666 logical_items,
7667 fragment_constraints,
7668 hyphenator.as_ref(),
7669 fonts,
7670 );
7671 }
7672
7673 let hyphenator = if fragment_constraints.hyphenation == Hyphens::Auto {
7675 fragment_constraints
7676 .hyphenation_language
7677 .and_then(|lang| get_hyphenator(lang).ok())
7678 } else {
7679 None
7680 };
7681
7682 let mut positioned_items = Vec::new();
7683 let mut layout_bounds = Rect::default();
7684
7685 let num_columns = fragment_constraints.columns.max(1);
7686 let total_column_gap = fragment_constraints.column_gap * (num_columns - 1) as f32;
7687
7688 let is_min_content = matches!(fragment_constraints.available_width, AvailableSpace::MinContent);
7701 let is_max_content = matches!(fragment_constraints.available_width, AvailableSpace::MaxContent);
7702
7703 let column_width = match fragment_constraints.available_width {
7704 AvailableSpace::Definite(width) => (width - total_column_gap) / num_columns as f32,
7705 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
7706 f32::MAX / 2.0
7709 }
7710 };
7711 let mut current_column = 0;
7712 if let Some(msgs) = debug_messages {
7713 msgs.push(LayoutDebugMessage::info(format!(
7714 "Column width calculated: {}",
7715 column_width
7716 )));
7717 }
7718
7719 let base_direction = if fragment_constraints.unicode_bidi == UnicodeBidi::Plaintext {
7725 let remaining = &cursor.items[cursor.next_item_index..];
7727 let text: String = remaining.iter()
7728 .filter_map(|i| i.as_cluster())
7729 .map(|c| c.text.as_str())
7730 .collect();
7731 match unicode_bidi::get_base_direction(text.as_str()) {
7732 unicode_bidi::Direction::Ltr => BidiDirection::Ltr,
7733 unicode_bidi::Direction::Rtl => BidiDirection::Rtl,
7734 unicode_bidi::Direction::Mixed => fragment_constraints.direction.unwrap_or(BidiDirection::Ltr),
7736 }
7737 } else {
7738 fragment_constraints.direction.unwrap_or(BidiDirection::Ltr)
7739 };
7740
7741 if let Some(msgs) = debug_messages {
7742 msgs.push(LayoutDebugMessage::info(format!(
7743 "[PFLayout] Base direction: {:?} (from CSS), Text align: {:?}",
7744 base_direction, fragment_constraints.text_align
7745 )));
7746 }
7747
7748 'column_loop: while current_column < num_columns {
7749 if let Some(msgs) = debug_messages {
7750 msgs.push(LayoutDebugMessage::info(format!(
7751 "\n-- Starting Column {} --",
7752 current_column
7753 )));
7754 }
7755 let column_start_x =
7756 (column_width + fragment_constraints.column_gap) * current_column as f32;
7757 let mut line_top_y = 0.0;
7758 let mut line_index = 0;
7759 let mut empty_segment_count = 0; let mut is_after_forced_break = false;
7761 const MAX_EMPTY_SEGMENTS: usize = 1000; while !cursor.is_done() {
7764 if let Some(max_height) = fragment_constraints.available_height {
7765 if line_top_y >= max_height {
7766 if let Some(msgs) = debug_messages {
7767 msgs.push(LayoutDebugMessage::info(format!(
7768 " Column full (pen {} >= height {}), breaking to next column.",
7769 line_top_y, max_height
7770 )));
7771 }
7772 break;
7773 }
7774 }
7775
7776 if let Some(clamp) = fragment_constraints.line_clamp {
7777 if line_index >= clamp.get() {
7778 break;
7779 }
7780 }
7781
7782 let mut column_constraints = fragment_constraints.clone();
7784 if is_min_content {
7787 column_constraints.available_width = AvailableSpace::MinContent;
7788 } else if is_max_content {
7789 column_constraints.available_width = AvailableSpace::MaxContent;
7790 } else {
7791 column_constraints.available_width = AvailableSpace::Definite(column_width);
7792 }
7793 let line_constraints = get_line_constraints(
7794 line_top_y,
7795 fragment_constraints.resolved_line_height(),
7796 &column_constraints,
7797 debug_messages,
7798 );
7799
7800 if line_constraints.segments.is_empty() {
7801 empty_segment_count += 1;
7802 if let Some(msgs) = debug_messages {
7803 msgs.push(LayoutDebugMessage::info(format!(
7804 " No available segments at y={}, skipping to next line. (empty count: \
7805 {}/{})",
7806 line_top_y, empty_segment_count, MAX_EMPTY_SEGMENTS
7807 )));
7808 }
7809
7810 if empty_segment_count >= MAX_EMPTY_SEGMENTS {
7812 if let Some(msgs) = debug_messages {
7813 msgs.push(LayoutDebugMessage::warning(format!(
7814 " [WARN] Reached maximum empty segment count ({}). Breaking to \
7815 prevent infinite loop.",
7816 MAX_EMPTY_SEGMENTS
7817 )));
7818 msgs.push(LayoutDebugMessage::warning(
7819 " This likely means the shape constraints are too restrictive or \
7820 positioned incorrectly."
7821 .to_string(),
7822 ));
7823 msgs.push(LayoutDebugMessage::warning(format!(
7824 " Current y={}, shape boundaries might be outside this range.",
7825 line_top_y
7826 )));
7827 }
7828 break;
7829 }
7830
7831 if !fragment_constraints.shape_boundaries.is_empty() && empty_segment_count > 50 {
7834 let max_shape_y: f32 = fragment_constraints
7836 .shape_boundaries
7837 .iter()
7838 .map(|shape| {
7839 match shape {
7840 ShapeBoundary::Circle { center, radius } => center.y + radius,
7841 ShapeBoundary::Ellipse { center, radii } => center.y + radii.height,
7842 ShapeBoundary::Polygon { points } => {
7843 points.iter().map(|p| p.y).fold(0.0, f32::max)
7844 }
7845 ShapeBoundary::Rectangle(rect) => rect.y + rect.height,
7846 ShapeBoundary::Path { .. } => f32::MAX, }
7848 })
7849 .fold(0.0, f32::max);
7850
7851 if line_top_y > max_shape_y + 100.0 {
7852 if let Some(msgs) = debug_messages {
7853 msgs.push(LayoutDebugMessage::info(format!(
7854 " [INFO] Current y={} is far beyond maximum shape extent y={}. \
7855 Breaking layout.",
7856 line_top_y, max_shape_y
7857 )));
7858 msgs.push(LayoutDebugMessage::info(
7859 " Shape boundaries exist but no segments available - text cannot \
7860 fit in shape."
7861 .to_string(),
7862 ));
7863 }
7864 break;
7865 }
7866 }
7867
7868 line_top_y += fragment_constraints.resolved_line_height();
7869 continue;
7870 }
7871
7872 empty_segment_count = 0;
7874
7875 let effective_overflow_wrap = if is_min_content && fragment_constraints.overflow_wrap == OverflowWrap::Anywhere {
7880 OverflowWrap::Anywhere
7881 } else if is_min_content && fragment_constraints.overflow_wrap == OverflowWrap::BreakWord {
7882 OverflowWrap::Normal
7883 } else {
7884 fragment_constraints.overflow_wrap
7885 };
7886
7887 let (mut line_items, was_hyphenated) =
7894 break_one_line(cursor, &line_constraints, false, hyphenator.as_ref(), fonts, fragment_constraints.line_break, fragment_constraints.white_space_mode, effective_overflow_wrap);
7895 if line_items.is_empty() {
7896 if let Some(msgs) = debug_messages {
7897 msgs.push(LayoutDebugMessage::info(
7898 " Break returned no items. Ending column.".to_string(),
7899 ));
7900 }
7901 break;
7902 }
7903
7904 let line_text_before_rev: String = line_items
7905 .iter()
7906 .filter_map(|i| i.as_cluster())
7907 .map(|c| c.text.as_str())
7908 .collect();
7909 if let Some(msgs) = debug_messages {
7910 msgs.push(LayoutDebugMessage::info(format!(
7911 "[PFLayout] Line items from breaker (visual order): [{}]",
7913 line_text_before_rev
7914 )));
7915 }
7916
7917 let line_ends_with_forced_break = line_items.iter().any(|item| matches!(item, ShapedItem::Break { .. }));
7919
7920 let is_last_line = cursor.is_done() && !was_hyphenated;
7922 let effective_align = resolve_effective_alignment(
7923 fragment_constraints.text_align,
7924 fragment_constraints.text_align_last,
7925 is_last_line || line_ends_with_forced_break,
7926 );
7927
7928 let (mut line_pos_items, line_height) = position_one_line(
7929 line_items,
7930 &line_constraints,
7931 line_top_y,
7932 line_index,
7933 effective_align,
7934 base_direction,
7935 is_last_line,
7936 fragment_constraints,
7937 debug_messages,
7938 fonts,
7939 is_after_forced_break,
7940 );
7941
7942 is_after_forced_break = line_ends_with_forced_break;
7944
7945 for item in &mut line_pos_items {
7946 item.position.x += column_start_x;
7947 }
7948
7949 line_top_y += line_height.max(fragment_constraints.resolved_line_height());
7951 line_index += 1;
7952 positioned_items.extend(line_pos_items);
7953 }
7954 current_column += 1;
7955 }
7956
7957 if let Some(msgs) = debug_messages {
7958 msgs.push(LayoutDebugMessage::info(format!(
7959 "--- Exiting perform_fragment_layout, positioned {} items ---",
7960 positioned_items.len()
7961 )));
7962 }
7963
7964 let layout = UnifiedLayout {
7965 items: positioned_items,
7966 overflow: OverflowInfo::default(),
7967 };
7968
7969 let calculated_bounds = layout.bounds();
7971
7972 if let Some(msgs) = debug_messages {
7973 msgs.push(LayoutDebugMessage::info(format!(
7974 "--- Calculated bounds: width={}, height={} ---",
7975 calculated_bounds.width, calculated_bounds.height
7976 )));
7977 }
7978
7979 Ok(layout)
7980}
7981
7982pub fn break_one_line<T: ParsedFontTrait>(
8038 cursor: &mut BreakCursor,
8039 line_constraints: &LineConstraints,
8040 is_vertical: bool,
8041 hyphenator: Option<&Standard>,
8042 fonts: &LoadedFonts<T>,
8043 line_break: LineBreakStrictness,
8044 white_space_mode: WhiteSpaceMode,
8045 overflow_wrap: OverflowWrap,
8046) -> (Vec<ShapedItem>, bool) {
8047 let mut line_items = Vec::new();
8048 let mut current_width = 0.0;
8049
8050 if cursor.is_done() {
8051 return (Vec::new(), false);
8052 }
8053
8054 let break_spaces = white_space_mode == WhiteSpaceMode::BreakSpaces;
8059 if !break_spaces {
8060 while !cursor.is_done() {
8061 let next_unit = cursor.peek_next_unit();
8062 if next_unit.is_empty() {
8063 break;
8064 }
8065 if next_unit.len() == 1 && is_collapsible_whitespace(&next_unit[0]) {
8066 cursor.consume(1);
8067 } else {
8068 break;
8069 }
8070 }
8071 }
8072
8073 let no_wrap = matches!(white_space_mode, WhiteSpaceMode::Nowrap | WhiteSpaceMode::Pre);
8077
8078 if no_wrap {
8079 loop {
8082 let next_unit = cursor.peek_next_unit();
8083 if next_unit.is_empty() {
8084 break;
8085 }
8086 if let Some(ShapedItem::Break { .. }) = next_unit.first() {
8087 line_items.push(next_unit[0].clone());
8088 cursor.consume(1);
8089 return (line_items, false);
8090 }
8091 line_items.extend_from_slice(&next_unit);
8092 cursor.consume(next_unit.len());
8093 }
8094 } else {
8095
8096 loop {
8097 let next_unit = if line_break == LineBreakStrictness::Anywhere {
8099 cursor.peek_next_single_item()
8100 } else {
8101 cursor.peek_next_unit()
8102 };
8103 if next_unit.is_empty() {
8104 break; }
8106
8107 if let Some(ShapedItem::Break { .. }) = next_unit.first() {
8108 line_items.push(next_unit[0].clone());
8109 cursor.consume(1);
8110 return (line_items, false);
8111 }
8112
8113 let unit_width: f32 = next_unit
8114 .iter()
8115 .map(|item| get_item_measure(item, is_vertical))
8116 .sum();
8117 let available_width = line_constraints.total_available - current_width;
8118
8119 if unit_width <= available_width {
8121 line_items.extend_from_slice(&next_unit);
8122 current_width += unit_width;
8123 cursor.consume(next_unit.len());
8124 } else {
8125 if line_break != LineBreakStrictness::Anywhere {
8127 if let Some(hyphenator) = hyphenator {
8128 if !is_break_opportunity(next_unit.last().unwrap()) {
8129 if let Some(hyphenation_result) = try_hyphenate_word_cluster(
8130 &next_unit,
8131 available_width,
8132 is_vertical,
8133 hyphenator,
8134 fonts,
8135 ) {
8136 line_items.extend(hyphenation_result.line_part);
8137 cursor.consume(next_unit.len());
8138 cursor.partial_remainder = hyphenation_result.remainder_part;
8139 return (line_items, true);
8140 }
8141 }
8142 }
8143 }
8144
8145 if line_items.is_empty() {
8153 match overflow_wrap {
8154 OverflowWrap::Anywhere | OverflowWrap::BreakWord => {
8155 let avail = line_constraints.total_available;
8162 for item in next_unit.iter() {
8163 let item_w = get_item_measure(item, is_vertical);
8164 if !line_items.is_empty() && avail > 0.0 && current_width + item_w > avail {
8168 break;
8169 }
8170 line_items.push(item.clone());
8171 current_width += item_w;
8172 if avail <= 0.0 {
8177 continue; }
8179 }
8180 let consumed = line_items.len().max(1);
8181 if line_items.is_empty() {
8182 line_items.push(next_unit[0].clone());
8183 }
8184 cursor.consume(consumed);
8185 }
8186 OverflowWrap::Normal => {
8187 line_items.push(next_unit[0].clone());
8189 cursor.consume(1);
8190 }
8191 }
8192 }
8193 break;
8194 }
8195 }
8196
8197 } while let Some(last) = line_items.last() {
8204 if is_collapsible_whitespace(last) {
8205 line_items.pop();
8206 } else {
8207 break;
8208 }
8209 }
8210
8211 (line_items, false)
8212}
8213
8214#[derive(Clone)]
8216pub struct HyphenationBreak {
8217 pub char_len_on_line: usize,
8219 pub width_on_line: f32,
8221 pub line_part: Vec<ShapedItem>,
8223 pub hyphen_item: ShapedItem,
8225 pub remainder_part: Vec<ShapedItem>,
8228}
8229
8230pub fn find_all_hyphenation_breaks<T: ParsedFontTrait>(
8232 word_clusters: &[ShapedCluster],
8233 hyphenator: &Standard,
8234 is_vertical: bool, fonts: &LoadedFonts<T>,
8236) -> Option<Vec<HyphenationBreak>> {
8237 if word_clusters.is_empty() {
8238 return None;
8239 }
8240
8241 let mut word_string = String::new();
8243 let mut char_map = Vec::new();
8244 let mut current_width = 0.0;
8245
8246 for (cluster_idx, cluster) in word_clusters.iter().enumerate() {
8247 for (char_byte_offset, _ch) in cluster.text.char_indices() {
8248 let glyph_idx = cluster
8249 .glyphs
8250 .iter()
8251 .rposition(|g| g.cluster_offset as usize <= char_byte_offset)
8252 .unwrap_or(0);
8253 let glyph = &cluster.glyphs[glyph_idx];
8254
8255 let num_chars_in_glyph = cluster.text[glyph.cluster_offset as usize..]
8256 .chars()
8257 .count();
8258 let advance_per_char = if is_vertical {
8259 glyph.vertical_advance
8260 } else {
8261 glyph.advance
8262 } / (num_chars_in_glyph as f32).max(1.0);
8263
8264 current_width += advance_per_char;
8265 char_map.push((cluster_idx, glyph_idx, current_width));
8266 }
8267 word_string.push_str(&cluster.text);
8268 }
8269
8270 let opportunities = hyphenator.hyphenate(&word_string);
8273 if opportunities.breaks.is_empty() {
8274 return None;
8275 }
8276
8277 let last_cluster = word_clusters.last().unwrap();
8278 let last_glyph = last_cluster.glyphs.last().unwrap();
8279 let style = last_cluster.style.clone();
8280
8281 let font = fonts.get_by_hash(last_glyph.font_hash)?;
8283 let (hyphen_glyph_id, hyphen_advance) =
8284 font.get_hyphen_glyph_and_advance(style.font_size_px)?;
8285
8286 let mut possible_breaks = Vec::new();
8287
8288 for &break_char_idx in &opportunities.breaks {
8290 if break_char_idx == 0 || break_char_idx > char_map.len() {
8293 continue;
8294 }
8295
8296 let (_, _, width_at_break) = char_map[break_char_idx - 1];
8297
8298 let line_part: Vec<ShapedItem> = word_clusters[..break_char_idx]
8300 .iter()
8301 .map(|c| ShapedItem::Cluster(c.clone()))
8302 .collect();
8303
8304 let remainder_part: Vec<ShapedItem> = word_clusters[break_char_idx..]
8306 .iter()
8307 .map(|c| ShapedItem::Cluster(c.clone()))
8308 .collect();
8309
8310 let hyphen_item = ShapedItem::Cluster(ShapedCluster {
8311 text: "-".to_string(),
8312 source_cluster_id: GraphemeClusterId {
8313 source_run: u32::MAX,
8314 start_byte_in_run: u32::MAX,
8315 },
8316 source_content_index: ContentIndex {
8317 run_index: u32::MAX,
8318 item_index: u32::MAX,
8319 },
8320 source_node_id: None, glyphs: smallvec![ShapedGlyph {
8322 kind: GlyphKind::Hyphen,
8323 glyph_id: hyphen_glyph_id,
8324 font_hash: last_glyph.font_hash,
8325 font_metrics: last_glyph.font_metrics.clone(),
8326 cluster_offset: 0,
8327 script: Script::Latin,
8328 advance: hyphen_advance,
8329 kerning: 0.0,
8330 offset: Point::default(),
8331 style: style.clone(),
8332 vertical_advance: hyphen_advance,
8333 vertical_offset: Point::default(),
8334 }],
8335 advance: hyphen_advance,
8336 direction: BidiDirection::Ltr,
8337 style: style.clone(),
8338 marker_position_outside: None,
8339 is_first_fragment: true,
8340 is_last_fragment: true,
8341 });
8342
8343 possible_breaks.push(HyphenationBreak {
8344 char_len_on_line: break_char_idx,
8345 width_on_line: width_at_break + hyphen_advance,
8346 line_part,
8347 hyphen_item,
8348 remainder_part,
8349 });
8350 }
8351
8352 Some(possible_breaks)
8353}
8354
8355fn try_hyphenate_word_cluster<T: ParsedFontTrait>(
8357 word_items: &[ShapedItem],
8358 remaining_width: f32,
8359 is_vertical: bool,
8360 hyphenator: &Standard,
8361 fonts: &LoadedFonts<T>,
8362) -> Option<HyphenationResult> {
8363 let word_clusters: Vec<ShapedCluster> = word_items
8364 .iter()
8365 .filter_map(|item| item.as_cluster().cloned())
8366 .collect();
8367
8368 if word_clusters.is_empty() {
8369 return None;
8370 }
8371
8372 let all_breaks = find_all_hyphenation_breaks(&word_clusters, hyphenator, is_vertical, fonts)?;
8373
8374 if let Some(best_break) = all_breaks
8375 .into_iter()
8376 .rfind(|b| b.width_on_line <= remaining_width)
8377 {
8378 let mut line_part = best_break.line_part;
8379 line_part.push(best_break.hyphen_item);
8380
8381 return Some(HyphenationResult {
8382 line_part,
8383 remainder_part: best_break.remainder_part,
8384 });
8385 }
8386
8387 None
8388}
8389
8390pub fn position_one_line<T: ParsedFontTrait>(
8464 line_items: Vec<ShapedItem>,
8465 line_constraints: &LineConstraints,
8466 line_top_y: f32,
8467 line_index: usize,
8468 text_align: TextAlign,
8469 base_direction: BidiDirection,
8470 is_last_line: bool,
8471 constraints: &UnifiedConstraints,
8472 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
8473 fonts: &LoadedFonts<T>,
8474 is_after_forced_break: bool,
8475) -> (Vec<PositionedItem>, f32) {
8476 let line_text: String = line_items
8477 .iter()
8478 .filter_map(|i| i.as_cluster())
8479 .map(|c| c.text.as_str())
8480 .collect();
8481 if let Some(msgs) = debug_messages {
8482 msgs.push(LayoutDebugMessage::info(format!(
8483 "\n--- Entering position_one_line for line: [{}] ---",
8484 line_text
8485 )));
8486 }
8487 let physical_align = match (text_align, base_direction) {
8491 (TextAlign::Start, BidiDirection::Ltr) => TextAlign::Left,
8492 (TextAlign::Start, BidiDirection::Rtl) => TextAlign::Right,
8493 (TextAlign::End, BidiDirection::Ltr) => TextAlign::Right,
8494 (TextAlign::End, BidiDirection::Rtl) => TextAlign::Left,
8495 (other, _) => other,
8497 };
8498 if let Some(msgs) = debug_messages {
8499 msgs.push(LayoutDebugMessage::info(format!(
8500 "[Pos1Line] Physical align: {:?}",
8501 physical_align
8502 )));
8503 }
8504
8505 if line_items.is_empty() {
8509 return (Vec::new(), 0.0);
8510 }
8511 let mut positioned = Vec::new();
8512 let is_vertical = constraints.is_vertical();
8513
8514 let (content_ascent, content_descent) = calculate_line_metrics(&line_items, constraints.vertical_align, constraints);
8519
8520 let strut_ad = constraints.strut_ascent + constraints.strut_descent;
8527 let strut_leading_half = (constraints.resolved_line_height() - strut_ad) / 2.0;
8528 let strut_above = constraints.strut_ascent + strut_leading_half;
8529 let strut_below = constraints.strut_descent + strut_leading_half;
8530 let line_ascent = content_ascent.max(strut_above);
8531 let line_descent = content_descent.max(strut_below);
8532 let line_box_height = line_ascent + line_descent;
8533
8534 let line_baseline_y = line_top_y + line_ascent;
8536
8537 let mut item_cursor = 0;
8539 let is_first_line_of_para = line_index == 0; for (segment_idx, segment) in line_constraints.segments.iter().enumerate() {
8542 if item_cursor >= line_items.len() {
8543 break;
8544 }
8545
8546 let mut segment_items = Vec::new();
8548 let mut current_segment_width = 0.0;
8549 while item_cursor < line_items.len() {
8550 let item = &line_items[item_cursor];
8551 let item_measure = get_item_measure(item, is_vertical);
8552 if current_segment_width + item_measure > segment.width && !segment_items.is_empty() {
8554 break;
8555 }
8556 segment_items.push(item.clone());
8557 current_segment_width += item_measure;
8558 item_cursor += 1;
8559 }
8560
8561 if segment_items.is_empty() {
8562 continue;
8563 }
8564
8565 let (extra_word_spacing, extra_char_spacing) = if (constraints.text_align == TextAlign::Justify
8572 || constraints.text_align == TextAlign::JustifyAll)
8573 && constraints.text_justify != JustifyContent::None
8574 && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
8575 && constraints.text_justify != JustifyContent::Kashida
8576 {
8577 let segment_line_constraints = LineConstraints {
8578 segments: vec![segment.clone()],
8579 total_available: segment.width,
8580 };
8581 calculate_justification_spacing(
8582 &segment_items,
8583 &segment_line_constraints,
8584 constraints.text_justify,
8585 is_vertical,
8586 )
8587 } else {
8588 (0.0, 0.0)
8589 };
8590
8591 let justified_segment_items = if constraints.text_justify == JustifyContent::Kashida
8593 && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
8594 {
8595 let segment_line_constraints = LineConstraints {
8596 segments: vec![segment.clone()],
8597 total_available: segment.width,
8598 };
8599 justify_kashida_and_rebuild(
8600 segment_items,
8601 &segment_line_constraints,
8602 is_vertical,
8603 debug_messages,
8604 fonts,
8605 )
8606 } else {
8607 segment_items
8608 };
8609
8610 let final_segment_width: f32 = justified_segment_items
8612 .iter()
8613 .map(|item| get_item_measure(item, is_vertical))
8614 .sum();
8615
8616 let trailing_ws_width = match constraints.white_space_mode {
8628 WhiteSpaceMode::BreakSpaces | WhiteSpaceMode::Pre => 0.0,
8629 WhiteSpaceMode::Normal | WhiteSpaceMode::Nowrap | WhiteSpaceMode::PreLine => {
8630 measure_trailing_whitespace(&justified_segment_items, is_vertical)
8631 }
8632 WhiteSpaceMode::PreWrap => {
8634 let has_forced_break = justified_segment_items.last()
8635 .map(|item| matches!(item, ShapedItem::Break { .. }))
8636 .unwrap_or(false);
8637 let ws_width = measure_trailing_whitespace(&justified_segment_items, is_vertical);
8638 if has_forced_break {
8639 let content_width = final_segment_width - ws_width;
8642 if content_width + ws_width > segment.width {
8643 ws_width
8644 } else {
8645 0.0
8646 }
8647 } else {
8648 ws_width }
8650 }
8651 };
8652 let effective_segment_width = final_segment_width - trailing_ws_width;
8653
8654 let remaining_space = segment.width - effective_segment_width;
8657
8658 let is_indefinite_width = segment.width.is_infinite() || segment.width > 1e30;
8664 let alignment_offset = if is_indefinite_width {
8666 0.0 } else {
8668 match physical_align {
8669 TextAlign::Center => remaining_space / 2.0,
8670 TextAlign::Right => remaining_space,
8671 TextAlign::Justify | TextAlign::JustifyAll
8672 if remaining_space > 0.0
8673 && extra_word_spacing == 0.0
8674 && extra_char_spacing == 0.0 =>
8675 {
8676 remaining_space / 2.0
8679 }
8680 _ => 0.0, }
8682 };
8683
8684 let mut main_axis_pen = segment.start_x + alignment_offset;
8685 if let Some(msgs) = debug_messages {
8686 msgs.push(LayoutDebugMessage::info(format!(
8687 "[Pos1Line] Segment width: {}, Item width: {}, Remaining space: {}, Initial pen: \
8688 {}",
8689 segment.width, final_segment_width, remaining_space, main_axis_pen
8690 )));
8691 }
8692
8693 if segment_idx == 0 {
8696 let is_indent_target = if constraints.text_indent_each_line {
8697 is_first_line_of_para || is_after_forced_break
8699 } else {
8700 is_first_line_of_para
8702 };
8703 let should_indent = if constraints.text_indent_hanging {
8705 !is_indent_target
8706 } else {
8707 is_indent_target
8708 };
8709 if should_indent {
8710 main_axis_pen += constraints.text_indent;
8711 }
8712 }
8713
8714 let total_marker_width: f32 = justified_segment_items
8717 .iter()
8718 .filter_map(|item| {
8719 if let ShapedItem::Cluster(c) = item {
8720 if c.marker_position_outside == Some(true) {
8721 return Some(get_item_measure(item, is_vertical));
8722 }
8723 }
8724 None
8725 })
8726 .sum();
8727
8728 let marker_spacing = 4.0; let mut marker_pen = if total_marker_width > 0.0 {
8731 -(total_marker_width + marker_spacing)
8732 } else {
8733 0.0
8734 };
8735
8736 let inline_offsets: Vec<(f32, f32)> = {
8762 let items_slice: &[ShapedItem] = &justified_segment_items;
8763 items_slice.iter().enumerate().map(|(idx, item)| {
8764 if let ShapedItem::Cluster(c) = item {
8765 if let Some(border) = c.style.border.as_ref() {
8766 if border.has_chrome() {
8767 let style_ptr = Arc::as_ptr(&c.style);
8768 let prev_same_span = idx > 0 && items_slice[idx - 1]
8769 .as_cluster()
8770 .map(|pc| Arc::as_ptr(&pc.style) == style_ptr)
8771 .unwrap_or(false);
8772 let next_same_span = idx + 1 < items_slice.len() && items_slice[idx + 1]
8773 .as_cluster()
8774 .map(|nc| Arc::as_ptr(&nc.style) == style_ptr)
8775 .unwrap_or(false);
8776 let left = if !prev_same_span { border.left_inset() } else { 0.0 };
8777 let right = if !next_same_span { border.right_inset() } else { 0.0 };
8778 return (left, right);
8779 }
8780 }
8781 }
8782 (0.0, 0.0)
8783 }).collect()
8784 };
8785 let mut inline_offset_idx = 0;
8786
8787 for item in justified_segment_items {
8788 let (item_ascent, item_descent) = get_item_vertical_metrics(&item, constraints);
8789 let effective_align = get_item_vertical_align(&item)
8791 .unwrap_or(constraints.vertical_align);
8792 let item_baseline_pos = match effective_align {
8796 VerticalAlign::Top => line_top_y + item_ascent,
8800 VerticalAlign::Middle => {
8802 let half_x_height = constraints.strut_x_height / 2.0;
8803 line_baseline_y + half_x_height - (item_ascent + item_descent) / 2.0 + item_ascent
8804 }
8805 VerticalAlign::Bottom => line_top_y + line_box_height - item_descent,
8807 VerticalAlign::Sub => line_baseline_y + line_ascent * 0.3,
8809 VerticalAlign::Super => line_baseline_y - line_ascent * 0.4,
8812 VerticalAlign::TextTop => (line_baseline_y - constraints.strut_ascent) + item_ascent,
8815 VerticalAlign::TextBottom => (line_baseline_y + constraints.strut_descent) - item_descent,
8818 VerticalAlign::Offset(offset) => line_baseline_y - offset,
8820 VerticalAlign::Baseline => line_baseline_y,
8824 };
8825
8826 let item_measure = get_item_measure(&item, is_vertical);
8828
8829 let (left_inset, right_inset) = if inline_offset_idx < inline_offsets.len() {
8831 inline_offsets[inline_offset_idx]
8832 } else {
8833 (0.0, 0.0)
8834 };
8835 inline_offset_idx += 1;
8836 main_axis_pen += left_inset;
8837
8838 let position = if is_vertical {
8839 Point {
8840 x: item_baseline_pos - item_ascent,
8841 y: main_axis_pen,
8842 }
8843 } else {
8844 if let Some(msgs) = debug_messages {
8845 msgs.push(LayoutDebugMessage::info(format!(
8846 "[Pos1Line] is_vertical=false, main_axis_pen={}, item_baseline_pos={}, \
8847 item_ascent={}",
8848 main_axis_pen, item_baseline_pos, item_ascent
8849 )));
8850 }
8851
8852 let x_position = if let ShapedItem::Cluster(cluster) = &item {
8854 if cluster.marker_position_outside == Some(true) {
8855 let marker_width = item_measure;
8857 if let Some(msgs) = debug_messages {
8858 msgs.push(LayoutDebugMessage::info(format!(
8859 "[Pos1Line] Outside marker detected! width={}, positioning at \
8860 marker_pen={}",
8861 marker_width, marker_pen
8862 )));
8863 }
8864 let pos = marker_pen;
8865 marker_pen += marker_width; pos
8867 } else {
8868 main_axis_pen
8869 }
8870 } else {
8871 main_axis_pen
8872 };
8873
8874 Point {
8875 y: item_baseline_pos - item_ascent,
8876 x: x_position,
8877 }
8878 };
8879
8880 let item_text = item
8882 .as_cluster()
8883 .map(|c| c.text.as_str())
8884 .unwrap_or("[OBJ]");
8885 if let Some(msgs) = debug_messages {
8886 msgs.push(LayoutDebugMessage::info(format!(
8887 "[Pos1Line] Positioning item '{}' at pen_x={}",
8888 item_text, main_axis_pen
8889 )));
8890 }
8891 positioned.push(PositionedItem {
8892 item: item.clone(),
8893 position,
8894 line_index,
8895 });
8896
8897 let is_outside_marker = if let ShapedItem::Cluster(c) = &item {
8899 c.marker_position_outside == Some(true)
8900 } else {
8901 false
8902 };
8903
8904 if !is_outside_marker {
8905 main_axis_pen += item_measure;
8906 main_axis_pen += right_inset;
8908 }
8909
8910 let is_cursive = if let ShapedItem::Cluster(c) = &item { is_cursive_script_cluster(c) } else { false };
8913 if !is_outside_marker && extra_char_spacing > 0.0 && can_justify_after(&item) && !is_cursive {
8914 main_axis_pen += extra_char_spacing;
8915 }
8916 if let ShapedItem::Cluster(c) = &item {
8920 if !is_outside_marker {
8921 if !is_cursive_script_cluster(c) {
8930 let letter_spacing_px = match c.style.letter_spacing {
8931 Spacing::Px(px) => px as f32,
8932 Spacing::Em(em) => em * c.style.font_size_px,
8933 };
8934 main_axis_pen += letter_spacing_px;
8935 }
8936 if is_word_separator(&item) {
8938 let word_spacing_px = match c.style.word_spacing {
8939 Spacing::Px(px) => px as f32,
8940 Spacing::Em(em) => em * c.style.font_size_px,
8941 };
8942 main_axis_pen += word_spacing_px;
8943 main_axis_pen += extra_word_spacing;
8944 }
8945 }
8946 }
8947 }
8948 }
8949
8950 (positioned, line_box_height)
8951}
8952
8953fn calculate_alignment_offset(
8955 items: &[ShapedItem],
8956 line_constraints: &LineConstraints,
8957 align: TextAlign,
8958 is_vertical: bool,
8959 constraints: &UnifiedConstraints,
8960) -> f32 {
8961 if let Some(segment) = line_constraints.segments.first() {
8963 let total_width: f32 = items
8964 .iter()
8965 .map(|item| get_item_measure(item, is_vertical))
8966 .sum();
8967
8968 let available_width = if constraints.segment_alignment == SegmentAlignment::Total {
8969 line_constraints.total_available
8970 } else {
8971 segment.width
8972 };
8973
8974 if total_width >= available_width {
8975 return 0.0; }
8977
8978 let remaining_space = available_width - total_width;
8979
8980 match align {
8981 TextAlign::Center => remaining_space / 2.0,
8982 TextAlign::Right => remaining_space,
8983 _ => 0.0, }
8985 } else {
8986 0.0
8987 }
8988}
8989
8990fn calculate_justification_spacing(
9008 items: &[ShapedItem],
9009 line_constraints: &LineConstraints,
9010 text_justify: JustifyContent,
9011 is_vertical: bool,
9012) -> (f32, f32) {
9013 let total_width: f32 = items
9015 .iter()
9016 .map(|item| get_item_measure(item, is_vertical))
9017 .sum();
9018 let available_width = line_constraints.total_available;
9019
9020 if total_width >= available_width || available_width <= 0.0 {
9021 return (0.0, 0.0);
9022 }
9023
9024 let extra_space = available_width - total_width;
9025
9026 match text_justify {
9028 JustifyContent::InterWord => {
9029 let space_count = items.iter().filter(|item| is_word_separator(item)).count();
9031 if space_count > 0 {
9032 (extra_space / space_count as f32, 0.0)
9033 } else {
9034 (0.0, 0.0) }
9036 }
9037 JustifyContent::InterCharacter | JustifyContent::Distribute => {
9038 let gap_count = items
9040 .iter()
9041 .enumerate()
9042 .filter(|(i, item)| *i < items.len() - 1 && can_justify_after(item))
9043 .count();
9044 if gap_count > 0 {
9045 (0.0, extra_space / gap_count as f32)
9046 } else {
9047 (0.0, 0.0) }
9049 }
9050 _ => (0.0, 0.0),
9052 }
9053}
9054
9055pub fn justify_kashida_and_rebuild<T: ParsedFontTrait>(
9061 items: Vec<ShapedItem>,
9062 line_constraints: &LineConstraints,
9063 is_vertical: bool,
9064 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
9065 fonts: &LoadedFonts<T>,
9066) -> Vec<ShapedItem> {
9067 if let Some(msgs) = debug_messages {
9068 msgs.push(LayoutDebugMessage::info(
9069 "\n--- Entering justify_kashida_and_rebuild ---".to_string(),
9070 ));
9071 }
9072 let total_width: f32 = items
9073 .iter()
9074 .map(|item| get_item_measure(item, is_vertical))
9075 .sum();
9076 let available_width = line_constraints.total_available;
9077 if let Some(msgs) = debug_messages {
9078 msgs.push(LayoutDebugMessage::info(format!(
9079 "Total item width: {}, Available width: {}",
9080 total_width, available_width
9081 )));
9082 }
9083
9084 if total_width >= available_width || available_width <= 0.0 {
9085 if let Some(msgs) = debug_messages {
9086 msgs.push(LayoutDebugMessage::info(
9087 "No justification needed (line is full or invalid).".to_string(),
9088 ));
9089 }
9090 return items;
9091 }
9092
9093 let extra_space = available_width - total_width;
9094 if let Some(msgs) = debug_messages {
9095 msgs.push(LayoutDebugMessage::info(format!(
9096 "Extra space to fill: {}",
9097 extra_space
9098 )));
9099 }
9100
9101 let font_info = items.iter().find_map(|item| {
9102 if let ShapedItem::Cluster(c) = item {
9103 if let Some(glyph) = c.glyphs.first() {
9104 if glyph.script == Script::Arabic {
9105 if let Some(font) = fonts.get_by_hash(glyph.font_hash) {
9107 return Some((
9108 font.clone(),
9109 glyph.font_hash,
9110 glyph.font_metrics.clone(),
9111 glyph.style.clone(),
9112 ));
9113 }
9114 }
9115 }
9116 }
9117 None
9118 });
9119
9120 let (font, font_hash, font_metrics, style) = match font_info {
9121 Some(info) => {
9122 if let Some(msgs) = debug_messages {
9123 msgs.push(LayoutDebugMessage::info(
9124 "Found Arabic font for kashida.".to_string(),
9125 ));
9126 }
9127 info
9128 }
9129 None => {
9130 if let Some(msgs) = debug_messages {
9131 msgs.push(LayoutDebugMessage::info(
9132 "No Arabic font found on line. Cannot insert kashidas.".to_string(),
9133 ));
9134 }
9135 return items;
9136 }
9137 };
9138
9139 let (kashida_glyph_id, kashida_advance) =
9140 match font.get_kashida_glyph_and_advance(style.font_size_px) {
9141 Some((id, adv)) if adv > 0.0 => {
9142 if let Some(msgs) = debug_messages {
9143 msgs.push(LayoutDebugMessage::info(format!(
9144 "Font provides kashida glyph with advance {}",
9145 adv
9146 )));
9147 }
9148 (id, adv)
9149 }
9150 _ => {
9151 if let Some(msgs) = debug_messages {
9152 msgs.push(LayoutDebugMessage::info(
9153 "Font does not support kashida justification.".to_string(),
9154 ));
9155 }
9156 return items;
9157 }
9158 };
9159
9160 let opportunity_indices: Vec<usize> = items
9161 .windows(2)
9162 .enumerate()
9163 .filter_map(|(i, window)| {
9164 if let (ShapedItem::Cluster(cur), ShapedItem::Cluster(next)) = (&window[0], &window[1])
9165 {
9166 if is_arabic_cluster(cur)
9167 && is_arabic_cluster(next)
9168 && !is_word_separator(&window[1])
9169 {
9170 return Some(i + 1);
9171 }
9172 }
9173 None
9174 })
9175 .collect();
9176
9177 if let Some(msgs) = debug_messages {
9178 msgs.push(LayoutDebugMessage::info(format!(
9179 "Found {} kashida insertion opportunities at indices: {:?}",
9180 opportunity_indices.len(),
9181 opportunity_indices
9182 )));
9183 }
9184
9185 if opportunity_indices.is_empty() {
9186 if let Some(msgs) = debug_messages {
9187 msgs.push(LayoutDebugMessage::info(
9188 "No opportunities found. Exiting.".to_string(),
9189 ));
9190 }
9191 return items;
9192 }
9193
9194 let num_kashidas_to_insert = (extra_space / kashida_advance).floor() as usize;
9195 if let Some(msgs) = debug_messages {
9196 msgs.push(LayoutDebugMessage::info(format!(
9197 "Calculated number of kashidas to insert: {}",
9198 num_kashidas_to_insert
9199 )));
9200 }
9201
9202 if num_kashidas_to_insert == 0 {
9203 return items;
9204 }
9205
9206 let kashidas_per_point = num_kashidas_to_insert / opportunity_indices.len();
9207 let mut remainder = num_kashidas_to_insert % opportunity_indices.len();
9208 if let Some(msgs) = debug_messages {
9209 msgs.push(LayoutDebugMessage::info(format!(
9210 "Distributing kashidas: {} per point, with {} remainder.",
9211 kashidas_per_point, remainder
9212 )));
9213 }
9214
9215 let kashida_item = {
9216 let kashida_glyph = ShapedGlyph {
9218 kind: GlyphKind::Kashida {
9219 width: kashida_advance,
9220 },
9221 glyph_id: kashida_glyph_id,
9222 font_hash,
9223 font_metrics: font_metrics.clone(),
9224 style: style.clone(),
9225 script: Script::Arabic,
9226 advance: kashida_advance,
9227 kerning: 0.0,
9228 cluster_offset: 0,
9229 offset: Point::default(),
9230 vertical_advance: 0.0,
9231 vertical_offset: Point::default(),
9232 };
9233 ShapedItem::Cluster(ShapedCluster {
9234 text: "\u{0640}".to_string(),
9235 source_cluster_id: GraphemeClusterId {
9236 source_run: u32::MAX,
9237 start_byte_in_run: u32::MAX,
9238 },
9239 source_content_index: ContentIndex {
9240 run_index: u32::MAX,
9241 item_index: u32::MAX,
9242 },
9243 source_node_id: None, glyphs: smallvec![kashida_glyph],
9245 advance: kashida_advance,
9246 direction: BidiDirection::Ltr,
9247 style,
9248 marker_position_outside: None,
9249 is_first_fragment: true,
9250 is_last_fragment: true,
9251 })
9252 };
9253
9254 let mut new_items = Vec::with_capacity(items.len() + num_kashidas_to_insert);
9255 let mut last_copy_idx = 0;
9256 for &point in &opportunity_indices {
9257 new_items.extend_from_slice(&items[last_copy_idx..point]);
9258 let mut num_to_insert = kashidas_per_point;
9259 if remainder > 0 {
9260 num_to_insert += 1;
9261 remainder -= 1;
9262 }
9263 for _ in 0..num_to_insert {
9264 new_items.push(kashida_item.clone());
9265 }
9266 last_copy_idx = point;
9267 }
9268 new_items.extend_from_slice(&items[last_copy_idx..]);
9269
9270 if let Some(msgs) = debug_messages {
9271 msgs.push(LayoutDebugMessage::info(format!(
9272 "--- Exiting justify_kashida_and_rebuild, new item count: {} ---",
9273 new_items.len()
9274 )));
9275 }
9276 new_items
9277}
9278
9279fn is_arabic_cluster(cluster: &ShapedCluster) -> bool {
9281 cluster.glyphs.iter().any(|g| g.script == Script::Arabic)
9284}
9285
9286fn measure_trailing_whitespace(items: &[ShapedItem], is_vertical: bool) -> f32 {
9288 let mut trailing_ws = 0.0;
9289 for item in items.iter().rev() {
9290 if is_collapsible_whitespace(item) {
9291 trailing_ws += get_item_measure(item, is_vertical);
9292 } else {
9293 break;
9294 }
9295 }
9296 trailing_ws
9297}
9298
9299pub fn is_collapsible_whitespace(item: &ShapedItem) -> bool {
9303 if let ShapedItem::Cluster(c) = item {
9304 c.text.chars().all(|ch| matches!(ch,
9305 ' ' | '\t' | '\u{1680}' ))
9307 } else {
9308 false
9309 }
9310}
9311
9312pub fn is_cursive_script_cluster(c: &ShapedCluster) -> bool {
9317 c.text.chars().next().map_or(false, |ch| is_cursive_script_char(ch))
9318}
9319
9320fn is_cursive_script_char(ch: char) -> bool {
9321 let cp = ch as u32;
9322 if (0x0600..=0x06FF).contains(&cp) { return true; }
9324 if (0x0750..=0x077F).contains(&cp) { return true; }
9325 if (0x08A0..=0x08FF).contains(&cp) { return true; }
9326 if (0xFB50..=0xFDFF).contains(&cp) { return true; }
9327 if (0xFE70..=0xFEFF).contains(&cp) { return true; }
9328 if (0x0700..=0x074F).contains(&cp) { return true; }
9330 if (0x1800..=0x18AF).contains(&cp) { return true; }
9332 if (0x07C0..=0x07FF).contains(&cp) { return true; }
9334 if (0x0840..=0x085F).contains(&cp) { return true; }
9336 if (0xA840..=0xA87F).contains(&cp) { return true; }
9338 if (0x10D00..=0x10D3F).contains(&cp) { return true; }
9340 false
9341}
9342
9343pub fn is_word_separator(item: &ShapedItem) -> bool {
9345 if let ShapedItem::Cluster(c) = item {
9346 c.text.chars().any(|g| is_word_separator_char(g))
9347 } else {
9348 false
9349 }
9350}
9351
9352fn is_word_separator_char(c: char) -> bool {
9358 match c {
9359 '\u{0020}' => true,
9361 '\u{00A0}' => true,
9363 '\u{1680}' => true,
9365 '\u{1361}' => true,
9367 '\u{2000}'..='\u{200A}' => false,
9369 '\u{202F}' => true,
9371 '\u{205F}' => true,
9373 '\u{3000}' => false,
9375 '\u{10100}' => true,
9377 '\u{10101}' => true,
9379 '\u{1039F}' => true,
9381 '\u{1091F}' => true,
9383 _ => false,
9385 }
9386}
9387
9388pub fn is_zero_width_space(item: &ShapedItem) -> bool {
9393 if let ShapedItem::Cluster(c) = item {
9394 c.text.contains('\u{200B}')
9395 } else {
9396 false
9397 }
9398}
9399
9400fn can_justify_after(item: &ShapedItem) -> bool {
9402 if let ShapedItem::Cluster(c) = item {
9403 c.text.chars().last().map_or(false, |g| {
9404 !g.is_whitespace() && classify_character(g as u32) != CharacterClass::Combining
9405 })
9406 } else {
9407 false
9411 }
9412}
9413
9414fn classify_character(codepoint: u32) -> CharacterClass {
9418 match codepoint {
9419 0x0020 | 0x00A0 | 0x3000 => CharacterClass::Space,
9420 0x0021..=0x002F | 0x003A..=0x0040 | 0x005B..=0x0060 | 0x007B..=0x007E => {
9421 CharacterClass::Punctuation
9422 }
9423 0x4E00..=0x9FFF | 0x3400..=0x4DBF => CharacterClass::Ideograph,
9424 0x0300..=0x036F | 0x1AB0..=0x1AFF => CharacterClass::Combining,
9425 0x1800..=0x18AF => CharacterClass::Letter,
9427 _ => CharacterClass::Letter,
9428 }
9429}
9430
9431pub fn get_item_measure(item: &ShapedItem, is_vertical: bool) -> f32 {
9433 match item {
9434 ShapedItem::Cluster(c) => {
9435 let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
9439 c.advance + total_kerning
9440 }
9441 ShapedItem::Object { bounds, .. }
9442 | ShapedItem::CombinedBlock { bounds, .. }
9443 | ShapedItem::Tab { bounds, .. } => {
9444 if is_vertical {
9445 bounds.height
9446 } else {
9447 bounds.width
9448 }
9449 }
9450 ShapedItem::Break { .. } => 0.0,
9451 }
9452}
9453
9454fn get_line_constraints(
9457 line_y: f32,
9458 line_height: f32,
9459 constraints: &UnifiedConstraints,
9460 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
9461) -> LineConstraints {
9462 if let Some(msgs) = debug_messages {
9463 msgs.push(LayoutDebugMessage::info(format!(
9464 "\n--- Entering get_line_constraints for y={} ---",
9465 line_y
9466 )));
9467 }
9468
9469 let mut available_segments = Vec::new();
9470 if constraints.shape_boundaries.is_empty() {
9471 let segment_width = match constraints.available_width {
9482 AvailableSpace::Definite(w) => w, AvailableSpace::MaxContent => f32::MAX / 2.0, AvailableSpace::MinContent => f32::MAX / 2.0, };
9486 available_segments.push(LineSegment {
9489 start_x: 0.0,
9490 width: segment_width,
9491 priority: 0,
9492 });
9493 } else {
9494 }
9496
9497 if let Some(msgs) = debug_messages {
9498 msgs.push(LayoutDebugMessage::info(format!(
9499 "Initial available segments: {:?}",
9500 available_segments
9501 )));
9502 }
9503
9504 for (idx, exclusion) in constraints.shape_exclusions.iter().enumerate() {
9505 if let Some(msgs) = debug_messages {
9506 msgs.push(LayoutDebugMessage::info(format!(
9507 "Applying exclusion #{}: {:?}",
9508 idx, exclusion
9509 )));
9510 }
9511 let exclusion_spans =
9512 get_shape_horizontal_spans(exclusion, line_y, line_height).unwrap_or_default();
9513 if let Some(msgs) = debug_messages {
9514 msgs.push(LayoutDebugMessage::info(format!(
9515 " Exclusion spans at y={}: {:?}",
9516 line_y, exclusion_spans
9517 )));
9518 }
9519
9520 if exclusion_spans.is_empty() {
9521 continue;
9522 }
9523
9524 let mut next_segments = Vec::new();
9525 for (excl_start, excl_end) in exclusion_spans {
9526 for segment in &available_segments {
9527 let seg_start = segment.start_x;
9528 let seg_end = segment.start_x + segment.width;
9529
9530 if seg_end > excl_start && seg_start < excl_end {
9532 if seg_start < excl_start {
9533 next_segments.push(LineSegment {
9535 start_x: seg_start,
9536 width: excl_start - seg_start,
9537 priority: segment.priority,
9538 });
9539 }
9540 if seg_end > excl_end {
9541 next_segments.push(LineSegment {
9543 start_x: excl_end,
9544 width: seg_end - excl_end,
9545 priority: segment.priority,
9546 });
9547 }
9548 } else {
9549 next_segments.push(segment.clone()); }
9551 }
9552 available_segments = merge_segments(next_segments);
9553 next_segments = Vec::new();
9554 }
9555 if let Some(msgs) = debug_messages {
9556 msgs.push(LayoutDebugMessage::info(format!(
9557 " Segments after exclusion #{}: {:?}",
9558 idx, available_segments
9559 )));
9560 }
9561 }
9562
9563 let total_width = available_segments.iter().map(|s| s.width).sum();
9564 if let Some(msgs) = debug_messages {
9565 msgs.push(LayoutDebugMessage::info(format!(
9566 "Final segments: {:?}, total available width: {}",
9567 available_segments, total_width
9568 )));
9569 msgs.push(LayoutDebugMessage::info(
9570 "--- Exiting get_line_constraints ---".to_string(),
9571 ));
9572 }
9573
9574 LineConstraints {
9575 segments: available_segments,
9576 total_available: total_width,
9577 }
9578}
9579
9580fn get_shape_horizontal_spans(
9583 shape: &ShapeBoundary,
9584 y: f32,
9585 line_height: f32,
9586) -> Result<Vec<(f32, f32)>, LayoutError> {
9587 match shape {
9588 ShapeBoundary::Rectangle(rect) => {
9589 let line_start = y;
9592 let line_end = y + line_height;
9593 let rect_start = rect.y;
9594 let rect_end = rect.y + rect.height;
9595
9596 if line_start < rect_end && line_end > rect_start {
9597 Ok(vec![(rect.x, rect.x + rect.width)])
9598 } else {
9599 Ok(vec![])
9600 }
9601 }
9602 ShapeBoundary::Circle { center, radius } => {
9603 let line_center_y = y + line_height / 2.0;
9604 let dy = (line_center_y - center.y).abs();
9605 if dy <= *radius {
9606 let dx = (radius.powi(2) - dy.powi(2)).sqrt();
9607 Ok(vec![(center.x - dx, center.x + dx)])
9608 } else {
9609 Ok(vec![])
9610 }
9611 }
9612 ShapeBoundary::Ellipse { center, radii } => {
9613 let line_center_y = y + line_height / 2.0;
9614 let dy = line_center_y - center.y;
9615 if dy.abs() <= radii.height {
9616 let y_term = dy / radii.height;
9618 let x_term_squared = 1.0 - y_term.powi(2);
9619 if x_term_squared >= 0.0 {
9620 let dx = radii.width * x_term_squared.sqrt();
9621 Ok(vec![(center.x - dx, center.x + dx)])
9622 } else {
9623 Ok(vec![])
9624 }
9625 } else {
9626 Ok(vec![])
9627 }
9628 }
9629 ShapeBoundary::Polygon { points } => {
9630 let segments = polygon_line_intersection(points, y, line_height)?;
9631 Ok(segments
9632 .iter()
9633 .map(|s| (s.start_x, s.start_x + s.width))
9634 .collect())
9635 }
9636 ShapeBoundary::Path { .. } => Ok(vec![]), }
9638}
9639
9640fn merge_segments(mut segments: Vec<LineSegment>) -> Vec<LineSegment> {
9642 if segments.len() <= 1 {
9643 return segments;
9644 }
9645 segments.sort_by(|a, b| a.start_x.partial_cmp(&b.start_x).unwrap());
9646 let mut merged = vec![segments[0].clone()];
9647 for next_seg in segments.iter().skip(1) {
9648 let last = merged.last_mut().unwrap();
9649 if next_seg.start_x <= last.start_x + last.width {
9650 let new_width = (next_seg.start_x + next_seg.width) - last.start_x;
9651 last.width = last.width.max(new_width);
9652 } else {
9653 merged.push(next_seg.clone());
9654 }
9655 }
9656 merged
9657}
9658
9659fn polygon_line_intersection(
9661 points: &[Point],
9662 y: f32,
9663 line_height: f32,
9664) -> Result<Vec<LineSegment>, LayoutError> {
9665 if points.len() < 3 {
9666 return Ok(vec![]);
9667 }
9668
9669 let line_center_y = y + line_height / 2.0;
9670 let mut intersections = Vec::new();
9671
9672 for i in 0..points.len() {
9674 let p1 = points[i];
9675 let p2 = points[(i + 1) % points.len()];
9676
9677 if (p2.y - p1.y).abs() < f32::EPSILON {
9679 continue;
9680 }
9681
9682 let crosses = (p1.y <= line_center_y && p2.y > line_center_y)
9684 || (p1.y > line_center_y && p2.y <= line_center_y);
9685
9686 if crosses {
9687 let t = (line_center_y - p1.y) / (p2.y - p1.y);
9689 let x = p1.x + t * (p2.x - p1.x);
9690 intersections.push(x);
9691 }
9692 }
9693
9694 intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
9696
9697 let mut segments = Vec::new();
9699 for chunk in intersections.chunks_exact(2) {
9700 let start_x = chunk[0];
9701 let end_x = chunk[1];
9702 if end_x > start_x {
9703 segments.push(LineSegment {
9704 start_x,
9705 width: end_x - start_x,
9706 priority: 0,
9707 });
9708 }
9709 }
9710
9711 Ok(segments)
9712}
9713
9714#[cfg(feature = "text_layout_hyphenation")]
9718fn get_hyphenator(language: HyphenationLanguage) -> Result<Standard, LayoutError> {
9719 Standard::from_embedded(language).map_err(|e| LayoutError::HyphenationError(e.to_string()))
9720}
9721
9722#[cfg(not(feature = "text_layout_hyphenation"))]
9724fn get_hyphenator(_language: Language) -> Result<Standard, LayoutError> {
9725 Err(LayoutError::HyphenationError("Hyphenation feature not enabled".to_string()))
9726}
9727
9728fn is_break_suppressing_control(ch: char) -> bool {
9731 matches!(ch,
9732 '\u{200D}' | '\u{2060}' | '\u{FEFF}' )
9736}
9737
9738fn is_break_forcing_control(ch: char) -> bool {
9739 matches!(ch,
9740 '\u{200B}' | '\u{2028}' | '\u{2029}' )
9744}
9745
9746fn is_cjk_character(ch: char) -> bool {
9749 let cp = ch as u32;
9750 matches!(cp,
9751 0x4E00..=0x9FFF |
9753 0x3400..=0x4DBF |
9755 0x20000..=0x2A6DF |
9757 0xF900..=0xFAFF |
9759 0x3040..=0x309F |
9761 0x30A0..=0x30FF |
9763 0x31F0..=0x31FF |
9765 0x3000..=0x303F |
9767 0xFF00..=0xFFEF |
9769 0xAC00..=0xD7AF
9771 )
9772}
9773
9774fn is_cjk_cluster(cluster: &ShapedCluster) -> bool {
9776 cluster.text.chars().any(is_cjk_character)
9777}
9778
9779fn is_break_opportunity_with_word_break(item: &ShapedItem, word_break: WordBreak, hyphens: Hyphens) -> bool {
9792 if is_word_separator(item) {
9794 return true;
9795 }
9796 if let ShapedItem::Break { .. } = item {
9797 return true;
9798 }
9799 if is_zero_width_space(item) {
9805 return true;
9806 }
9807 if hyphens != Hyphens::None {
9809 if let ShapedItem::Cluster(c) = item {
9810 if c.text.starts_with('\u{00AD}') {
9811 return true;
9812 }
9813 }
9814 }
9815
9816 match word_break {
9818 WordBreak::Normal => {
9819 if let ShapedItem::Cluster(c) = item {
9821 if is_cjk_cluster(c) {
9822 return true;
9823 }
9824 }
9825 false
9826 }
9827 WordBreak::BreakAll => {
9828 if let ShapedItem::Cluster(_) = item {
9830 return true;
9831 }
9832 false
9833 }
9834 WordBreak::KeepAll => {
9835 false
9838 }
9839 }
9840}
9841
9842fn is_cjk_break_allowed_by_strictness(
9852 ch: char,
9853 _prev_ch: Option<char>,
9854 strictness: LineBreakStrictness,
9855) -> bool {
9856 match strictness {
9857 LineBreakStrictness::Anywhere => true,
9858 LineBreakStrictness::Loose => {
9859 true
9862 }
9863 LineBreakStrictness::Normal | LineBreakStrictness::Auto => {
9864 match ch {
9868 '\u{2010}' | '\u{2013}' => false, _ => true,
9870 }
9871 }
9872 LineBreakStrictness::Strict => {
9873 match ch {
9878 '\u{301C}' | '\u{30A0}' => false, '\u{2010}' | '\u{2013}' => false, c if is_small_kana(c) => false,
9881 _ => true,
9882 }
9883 }
9884 }
9885}
9886
9887fn is_small_kana(ch: char) -> bool {
9890 matches!(ch,
9891 '\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}' )
9917}
9918
9919fn is_break_opportunity(item: &ShapedItem) -> bool {
9922 if matches!(item, ShapedItem::Object { .. } | ShapedItem::CombinedBlock { .. }) {
9925 return true;
9926 }
9927 if let ShapedItem::Cluster(c) = item {
9931 if c.text.contains('\u{200B}') {
9933 return true;
9934 }
9935 if c.text.chars().any(|ch| is_break_forcing_control(ch)) {
9937 return true;
9938 }
9939 if c.text.chars().any(|ch| matches!(ch, '\u{2060}' | '\u{200D}' | '\u{00A0}')) {
9941 return false;
9942 }
9943 if c.text.ends_with('\u{002D}') || c.text.ends_with('\u{2010}') {
9947 return true;
9948 }
9949 }
9950 is_break_opportunity_with_word_break(item, WordBreak::Normal, Hyphens::Manual)
9951}
9952
9953pub struct BreakCursor<'a> {
9956 pub items: &'a [ShapedItem],
9958 pub next_item_index: usize,
9960 pub partial_remainder: Vec<ShapedItem>,
9963 pub word_break: WordBreak,
9965 pub hyphens: Hyphens,
9966 pub line_break: LineBreakStrictness,
9967}
9968
9969impl<'a> BreakCursor<'a> {
9970 pub fn new(items: &'a [ShapedItem]) -> Self {
9971 Self {
9972 items,
9973 next_item_index: 0,
9974 partial_remainder: Vec::new(),
9975 word_break: WordBreak::Normal,
9976 hyphens: Hyphens::default(),
9977 line_break: LineBreakStrictness::default(),
9978 }
9979 }
9980
9981 pub fn with_word_break(items: &'a [ShapedItem], word_break: WordBreak) -> Self {
9982 Self {
9983 items,
9984 next_item_index: 0,
9985 partial_remainder: Vec::new(),
9986 word_break,
9987 hyphens: Hyphens::default(),
9988 line_break: LineBreakStrictness::default(),
9989 }
9990 }
9991
9992 pub fn is_at_start(&self) -> bool {
9994 self.next_item_index == 0 && self.partial_remainder.is_empty()
9995 }
9996
9997 pub fn drain_remaining(&mut self) -> Vec<ShapedItem> {
9999 let mut remaining = std::mem::take(&mut self.partial_remainder);
10000 if self.next_item_index < self.items.len() {
10001 remaining.extend_from_slice(&self.items[self.next_item_index..]);
10002 }
10003 self.next_item_index = self.items.len();
10004 remaining
10005 }
10006
10007 pub fn is_done(&self) -> bool {
10009 self.next_item_index >= self.items.len() && self.partial_remainder.is_empty()
10010 }
10011
10012 pub fn consume(&mut self, count: usize) {
10014 if count == 0 {
10015 return;
10016 }
10017
10018 let remainder_len = self.partial_remainder.len();
10019 if count <= remainder_len {
10020 self.partial_remainder.drain(..count);
10022 } else {
10023 let from_main_list = count - remainder_len;
10025 self.partial_remainder.clear();
10026 self.next_item_index += from_main_list;
10027 }
10028 }
10029
10030 pub fn peek_next_unit(&self) -> Vec<ShapedItem> {
10037 let mut unit = Vec::new();
10038 let mut source_items = self.partial_remainder.clone();
10039 source_items.extend_from_slice(&self.items[self.next_item_index..]);
10040
10041 if source_items.is_empty() {
10042 return unit;
10043 }
10044
10045 if is_break_opportunity_with_word_break(&source_items[0], self.word_break, self.hyphens) {
10047 unit.push(source_items[0].clone());
10048 return unit;
10049 }
10050
10051 let mut suppress_next_break = false;
10058 for (i, item) in source_items.iter().enumerate() {
10059 let starts_with_suppress = if let ShapedItem::Cluster(c) = item {
10062 c.text.chars().next().map_or(false, |ch| is_break_suppressing_control(ch))
10063 } else {
10064 false
10065 };
10066 let cjk_strictness_suppressed = if let ShapedItem::Cluster(c) = item {
10068 c.text.chars().next().map_or(false, |ch| {
10069 !is_cjk_break_allowed_by_strictness(ch, None, self.line_break)
10070 })
10071 } else {
10072 false
10073 };
10074 if i > 0 && !suppress_next_break && !starts_with_suppress && !cjk_strictness_suppressed && is_break_opportunity_with_word_break(item, self.word_break, self.hyphens) {
10075 break;
10076 }
10077 suppress_next_break = false;
10078 unit.push(item.clone());
10079
10080 if let ShapedItem::Cluster(c) = item {
10082 if let Some(last_ch) = c.text.chars().last() {
10083 if is_break_suppressing_control(last_ch) {
10084 suppress_next_break = true;
10085 }
10086 }
10087 }
10088
10089 if self.word_break == WordBreak::BreakAll {
10091 if let ShapedItem::Cluster(_) = item {
10092 break;
10093 }
10094 }
10095 }
10096 unit
10097 }
10098
10099 pub fn peek_next_single_item(&self) -> Vec<ShapedItem> {
10100 if !self.partial_remainder.is_empty() {
10101 return vec![self.partial_remainder[0].clone()];
10102 }
10103 if self.next_item_index < self.items.len() {
10104 return vec![self.items[self.next_item_index].clone()];
10105 }
10106 Vec::new()
10107 }
10108}
10109
10110struct HyphenationResult {
10112 line_part: Vec<ShapedItem>,
10114 remainder_part: Vec<ShapedItem>,
10116}
10117
10118fn perform_bidi_analysis<'a, 'b: 'a>(
10119 styled_runs: &'a [TextRunInfo],
10120 full_text: &'b str,
10121 force_lang: Option<Language>,
10122) -> Result<(Vec<VisualRun<'a>>, BidiDirection), LayoutError> {
10123 if full_text.is_empty() {
10124 return Ok((Vec::new(), BidiDirection::Ltr));
10125 }
10126
10127 let bidi_info = BidiInfo::new(full_text, None);
10128 let para = &bidi_info.paragraphs[0];
10129 let base_direction = if para.level.is_rtl() {
10130 BidiDirection::Rtl
10131 } else {
10132 BidiDirection::Ltr
10133 };
10134
10135 let mut byte_to_run_index: Vec<usize> = vec![0; full_text.len()];
10137 for (run_idx, run) in styled_runs.iter().enumerate() {
10138 let start = run.logical_start;
10139 let end = start + run.text.len();
10140 for i in start..end {
10141 byte_to_run_index[i] = run_idx;
10142 }
10143 }
10144
10145 let mut final_visual_runs = Vec::new();
10146 let (levels, visual_run_ranges) = bidi_info.visual_runs(para, para.range.clone());
10147
10148 for range in visual_run_ranges {
10149 let bidi_level = levels[range.start];
10150 let mut sub_run_start = range.start;
10151
10152 for i in (range.start + 1)..range.end {
10154 if byte_to_run_index[i] != byte_to_run_index[sub_run_start] {
10155 let original_run_idx = byte_to_run_index[sub_run_start];
10157 let script = crate::text3::script::detect_script(&full_text[sub_run_start..i])
10158 .unwrap_or(Script::Latin);
10159 final_visual_runs.push(VisualRun {
10160 text_slice: &full_text[sub_run_start..i],
10161 style: styled_runs[original_run_idx].style.clone(),
10162 logical_start_byte: sub_run_start,
10163 bidi_level: BidiLevel::new(bidi_level.number()),
10164 language: force_lang.unwrap_or_else(|| {
10165 crate::text3::script::script_to_language(
10166 script,
10167 &full_text[sub_run_start..i],
10168 )
10169 }),
10170 script,
10171 });
10172 sub_run_start = i;
10174 }
10175 }
10176
10177 let original_run_idx = byte_to_run_index[sub_run_start];
10179 let script = crate::text3::script::detect_script(&full_text[sub_run_start..range.end])
10180 .unwrap_or(Script::Latin);
10181
10182 final_visual_runs.push(VisualRun {
10183 text_slice: &full_text[sub_run_start..range.end],
10184 style: styled_runs[original_run_idx].style.clone(),
10185 logical_start_byte: sub_run_start,
10186 bidi_level: BidiLevel::new(bidi_level.number()),
10187 script,
10188 language: force_lang.unwrap_or_else(|| {
10189 crate::text3::script::script_to_language(
10190 script,
10191 &full_text[sub_run_start..range.end],
10192 )
10193 }),
10194 });
10195 }
10196
10197 Ok((final_visual_runs, base_direction))
10198}
10199
10200fn get_justification_priority(class: CharacterClass) -> u8 {
10201 match class {
10202 CharacterClass::Space => 0,
10203 CharacterClass::Punctuation => 64,
10204 CharacterClass::Ideograph => 128,
10205 CharacterClass::Letter => 192,
10206 CharacterClass::Symbol => 224,
10207 CharacterClass::Combining => 255,
10208 }
10209}