1use std::{
2 any::{Any, TypeId},
3 cmp::Ordering,
4 collections::{
5 hash_map::{DefaultHasher, Entry, HashMap},
6 BTreeSet,
7 },
8 hash::{Hash, Hasher},
9 mem::discriminant,
10 num::NonZeroUsize,
11 sync::{Arc, Mutex},
12};
13
14pub use azul_core::selection::{ContentIndex, GraphemeClusterId};
15use azul_core::{
16 dom::NodeId,
17 geom::{LogicalPosition, LogicalRect, LogicalSize},
18 resources::ImageRef,
19 selection::{CursorAffinity, SelectionRange, TextCursor},
20 ui_solver::GlyphInstance,
21};
22use azul_css::{
23 corety::LayoutDebugMessage, props::basic::ColorU, props::style::StyleBackgroundContent,
24};
25#[cfg(feature = "text_layout_hyphenation")]
26use hyphenation::{Hyphenator, Language as HyphenationLanguage, Load, Standard};
27use rust_fontconfig::{FcFontCache, FcPattern, FcWeight, FontId, PatternMatch, UnicodeRange};
28use unicode_bidi::{BidiInfo, Level, TextSource};
29use unicode_segmentation::UnicodeSegmentation;
30
31#[cfg(not(feature = "text_layout_hyphenation"))]
33pub struct Standard;
34
35#[cfg(not(feature = "text_layout_hyphenation"))]
36impl Standard {
37 pub fn hyphenate<'a>(&'a self, _word: &'a str) -> StubHyphenationBreaks {
39 StubHyphenationBreaks { breaks: Vec::new() }
40 }
41}
42
43#[cfg(not(feature = "text_layout_hyphenation"))]
45pub struct StubHyphenationBreaks {
46 pub breaks: alloc::vec::Vec<usize>,
47}
48
49use crate::text3::script::{script_to_language, Language, Script};
51
52#[derive(Debug, Clone, Copy, PartialEq)]
63pub enum AvailableSpace {
64 Definite(f32),
66 MinContent,
68 MaxContent,
70}
71
72impl Default for AvailableSpace {
73 fn default() -> Self {
74 AvailableSpace::Definite(0.0)
75 }
76}
77
78impl AvailableSpace {
79 pub fn is_definite(&self) -> bool {
81 matches!(self, AvailableSpace::Definite(_))
82 }
83
84 pub fn is_indefinite(&self) -> bool {
86 !self.is_definite()
87 }
88
89 pub fn unwrap_or(self, fallback: f32) -> f32 {
91 match self {
92 AvailableSpace::Definite(v) => v,
93 _ => fallback,
94 }
95 }
96
97 pub fn to_f32_for_layout(self) -> f32 {
103 match self {
104 AvailableSpace::Definite(v) => v,
105 AvailableSpace::MinContent => f32::MAX / 2.0,
106 AvailableSpace::MaxContent => f32::MAX / 2.0,
107 }
108 }
109
110 pub fn from_f32(value: f32) -> Self {
120 if value.is_infinite() || value >= f32::MAX / 2.0 {
121 AvailableSpace::MaxContent
123 } else if value <= 0.0 {
124 AvailableSpace::MinContent
126 } else {
127 AvailableSpace::Definite(value)
128 }
129 }
130}
131
132impl Hash for AvailableSpace {
133 fn hash<H: Hasher>(&self, state: &mut H) {
134 std::mem::discriminant(self).hash(state);
135 if let AvailableSpace::Definite(v) = self {
136 (v.round() as usize).hash(state);
137 }
138 }
139}
140
141pub use crate::font_traits::{ParsedFontTrait, ShallowClone};
143
144#[derive(Debug, Clone, PartialEq, Eq, Hash)]
148pub struct FontChainKey {
149 pub font_families: Vec<String>,
150 pub weight: FcWeight,
151 pub italic: bool,
152 pub oblique: bool,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Hash)]
161pub enum FontChainKeyOrRef {
162 Chain(FontChainKey),
164 Ref(usize),
166}
167
168impl FontChainKeyOrRef {
169 pub fn from_font_stack(font_stack: &FontStack) -> Self {
171 match font_stack {
172 FontStack::Stack(selectors) => FontChainKeyOrRef::Chain(FontChainKey::from_selectors(selectors)),
173 FontStack::Ref(font_ref) => FontChainKeyOrRef::Ref(font_ref.parsed as usize),
174 }
175 }
176
177 pub fn is_ref(&self) -> bool {
179 matches!(self, FontChainKeyOrRef::Ref(_))
180 }
181
182 pub fn as_ref_ptr(&self) -> Option<usize> {
184 match self {
185 FontChainKeyOrRef::Ref(ptr) => Some(*ptr),
186 _ => None,
187 }
188 }
189
190 pub fn as_chain(&self) -> Option<&FontChainKey> {
192 match self {
193 FontChainKeyOrRef::Chain(key) => Some(key),
194 _ => None,
195 }
196 }
197}
198
199impl FontChainKey {
200 pub fn from_selectors(font_stack: &[FontSelector]) -> Self {
202 let font_families: Vec<String> = font_stack
203 .iter()
204 .map(|s| s.family.clone())
205 .filter(|f| !f.is_empty())
206 .collect();
207
208 let font_families = if font_families.is_empty() {
209 vec!["serif".to_string()]
210 } else {
211 font_families
212 };
213
214 let weight = font_stack
215 .first()
216 .map(|s| s.weight)
217 .unwrap_or(FcWeight::Normal);
218 let is_italic = font_stack
219 .first()
220 .map(|s| s.style == FontStyle::Italic)
221 .unwrap_or(false);
222 let is_oblique = font_stack
223 .first()
224 .map(|s| s.style == FontStyle::Oblique)
225 .unwrap_or(false);
226
227 FontChainKey {
228 font_families,
229 weight,
230 italic: is_italic,
231 oblique: is_oblique,
232 }
233 }
234}
235
236#[derive(Debug, Clone)]
243pub struct LoadedFonts<T> {
244 pub fonts: HashMap<FontId, T>,
246 hash_to_id: HashMap<u64, FontId>,
248}
249
250impl<T: ParsedFontTrait> LoadedFonts<T> {
251 pub fn new() -> Self {
252 Self {
253 fonts: HashMap::new(),
254 hash_to_id: HashMap::new(),
255 }
256 }
257
258 pub fn insert(&mut self, font_id: FontId, font: T) {
260 let hash = font.get_hash();
261 self.hash_to_id.insert(hash, font_id.clone());
262 self.fonts.insert(font_id, font);
263 }
264
265 pub fn get(&self, font_id: &FontId) -> Option<&T> {
267 self.fonts.get(font_id)
268 }
269
270 pub fn get_by_hash(&self, hash: u64) -> Option<&T> {
272 self.hash_to_id.get(&hash).and_then(|id| self.fonts.get(id))
273 }
274
275 pub fn get_font_id_by_hash(&self, hash: u64) -> Option<&FontId> {
277 self.hash_to_id.get(&hash)
278 }
279
280 pub fn contains_key(&self, font_id: &FontId) -> bool {
282 self.fonts.contains_key(font_id)
283 }
284
285 pub fn contains_hash(&self, hash: u64) -> bool {
287 self.hash_to_id.contains_key(&hash)
288 }
289
290 pub fn iter(&self) -> impl Iterator<Item = (&FontId, &T)> {
292 self.fonts.iter()
293 }
294
295 pub fn len(&self) -> usize {
297 self.fonts.len()
298 }
299
300 pub fn is_empty(&self) -> bool {
302 self.fonts.is_empty()
303 }
304}
305
306impl<T: ParsedFontTrait> Default for LoadedFonts<T> {
307 fn default() -> Self {
308 Self::new()
309 }
310}
311
312impl<T: ParsedFontTrait> FromIterator<(FontId, T)> for LoadedFonts<T> {
313 fn from_iter<I: IntoIterator<Item = (FontId, T)>>(iter: I) -> Self {
314 let mut loaded = LoadedFonts::new();
315 for (id, font) in iter {
316 loaded.insert(id, font);
317 }
318 loaded
319 }
320}
321
322#[derive(Debug, Clone)]
327pub enum FontOrRef<T> {
328 Font(T),
330 Ref(azul_css::props::basic::FontRef),
332}
333
334impl<T: ParsedFontTrait> ShallowClone for FontOrRef<T> {
335 fn shallow_clone(&self) -> Self {
336 match self {
337 FontOrRef::Font(f) => FontOrRef::Font(f.shallow_clone()),
338 FontOrRef::Ref(r) => FontOrRef::Ref(r.clone()),
339 }
340 }
341}
342
343impl<T: ParsedFontTrait> ParsedFontTrait for FontOrRef<T> {
344 fn shape_text(
345 &self,
346 text: &str,
347 script: Script,
348 language: Language,
349 direction: BidiDirection,
350 style: &StyleProperties,
351 ) -> Result<Vec<Glyph>, LayoutError> {
352 match self {
353 FontOrRef::Font(f) => f.shape_text(text, script, language, direction, style),
354 FontOrRef::Ref(r) => r.shape_text(text, script, language, direction, style),
355 }
356 }
357
358 fn get_hash(&self) -> u64 {
359 match self {
360 FontOrRef::Font(f) => f.get_hash(),
361 FontOrRef::Ref(r) => r.get_hash(),
362 }
363 }
364
365 fn get_glyph_size(&self, glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
366 match self {
367 FontOrRef::Font(f) => f.get_glyph_size(glyph_id, font_size),
368 FontOrRef::Ref(r) => r.get_glyph_size(glyph_id, font_size),
369 }
370 }
371
372 fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
373 match self {
374 FontOrRef::Font(f) => f.get_hyphen_glyph_and_advance(font_size),
375 FontOrRef::Ref(r) => r.get_hyphen_glyph_and_advance(font_size),
376 }
377 }
378
379 fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
380 match self {
381 FontOrRef::Font(f) => f.get_kashida_glyph_and_advance(font_size),
382 FontOrRef::Ref(r) => r.get_kashida_glyph_and_advance(font_size),
383 }
384 }
385
386 fn has_glyph(&self, codepoint: u32) -> bool {
387 match self {
388 FontOrRef::Font(f) => f.has_glyph(codepoint),
389 FontOrRef::Ref(r) => r.has_glyph(codepoint),
390 }
391 }
392
393 fn get_vertical_metrics(&self, glyph_id: u16) -> Option<VerticalMetrics> {
394 match self {
395 FontOrRef::Font(f) => f.get_vertical_metrics(glyph_id),
396 FontOrRef::Ref(r) => r.get_vertical_metrics(glyph_id),
397 }
398 }
399
400 fn get_font_metrics(&self) -> LayoutFontMetrics {
401 match self {
402 FontOrRef::Font(f) => f.get_font_metrics(),
403 FontOrRef::Ref(r) => r.get_font_metrics(),
404 }
405 }
406
407 fn num_glyphs(&self) -> u16 {
408 match self {
409 FontOrRef::Font(f) => f.num_glyphs(),
410 FontOrRef::Ref(r) => r.num_glyphs(),
411 }
412 }
413}
414
415#[derive(Debug)]
416pub struct FontManager<T> {
417 pub fc_cache: Arc<FcFontCache>,
419 pub parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
423 pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
426 pub embedded_fonts: Mutex<HashMap<u64, azul_css::props::basic::FontRef>>,
429}
430
431impl<T: ParsedFontTrait> FontManager<T> {
432 pub fn new(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
433 Ok(Self {
434 fc_cache: Arc::new(fc_cache),
435 parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
436 font_chain_cache: HashMap::new(), embedded_fonts: Mutex::new(HashMap::new()),
438 })
439 }
440
441 pub fn from_arc(fc_cache: Arc<FcFontCache>) -> Result<Self, LayoutError> {
448 Ok(Self {
449 fc_cache,
450 parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
451 font_chain_cache: HashMap::new(),
452 embedded_fonts: Mutex::new(HashMap::new()),
453 })
454 }
455
456 pub fn from_arc_shared(
462 fc_cache: Arc<FcFontCache>,
463 parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
464 ) -> Result<Self, LayoutError> {
465 Ok(Self {
466 fc_cache,
467 parsed_fonts,
468 font_chain_cache: HashMap::new(),
469 embedded_fonts: Mutex::new(HashMap::new()),
470 })
471 }
472
473 pub fn shared_parsed_fonts(&self) -> Arc<Mutex<HashMap<FontId, T>>> {
478 Arc::clone(&self.parsed_fonts)
479 }
480
481 pub fn set_font_chain_cache(
486 &mut self,
487 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
488 ) {
489 self.font_chain_cache = chains;
490 }
491
492 pub fn merge_font_chain_cache(
496 &mut self,
497 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
498 ) {
499 self.font_chain_cache.extend(chains);
500 }
501
502 pub fn get_font_chain_cache(
504 &self,
505 ) -> &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain> {
506 &self.font_chain_cache
507 }
508
509 pub fn get_embedded_font_by_hash(&self, font_hash: u64) -> Option<azul_css::props::basic::FontRef> {
512 let embedded = self.embedded_fonts.lock().unwrap();
513 embedded.get(&font_hash).cloned()
514 }
515
516 pub fn get_font_by_hash(&self, font_hash: u64) -> Option<T> {
519 let parsed = self.parsed_fonts.lock().unwrap();
520 for (_, font) in parsed.iter() {
522 if font.get_hash() == font_hash {
523 return Some(font.clone());
524 }
525 }
526 None
527 }
528
529 pub fn register_embedded_font(&self, font_ref: &azul_css::props::basic::FontRef) {
532 let hash = font_ref.get_hash();
533 let mut embedded = self.embedded_fonts.lock().unwrap();
534 embedded.insert(hash, font_ref.clone());
535 }
536
537 pub fn get_loaded_fonts(&self) -> LoadedFonts<T> {
544 let parsed = self.parsed_fonts.lock().unwrap();
545 parsed
546 .iter()
547 .map(|(id, font)| (id.clone(), font.shallow_clone()))
548 .collect()
549 }
550
551 pub fn get_loaded_font_ids(&self) -> std::collections::HashSet<FontId> {
556 let parsed = self.parsed_fonts.lock().unwrap();
557 parsed.keys().cloned().collect()
558 }
559
560 pub fn insert_font(&self, font_id: FontId, font: T) -> Option<T> {
564 let mut parsed = self.parsed_fonts.lock().unwrap();
565 parsed.insert(font_id, font)
566 }
567
568 pub fn insert_fonts(&self, fonts: impl IntoIterator<Item = (FontId, T)>) {
573 let mut parsed = self.parsed_fonts.lock().unwrap();
574 for (font_id, font) in fonts {
575 parsed.insert(font_id, font);
576 }
577 }
578
579 pub fn remove_font(&self, font_id: &FontId) -> Option<T> {
583 let mut parsed = self.parsed_fonts.lock().unwrap();
584 parsed.remove(font_id)
585 }
586}
587
588#[derive(Debug, thiserror::Error)]
590pub enum LayoutError {
591 #[error("Bidi analysis failed: {0}")]
592 BidiError(String),
593 #[error("Shaping failed: {0}")]
594 ShapingError(String),
595 #[error("Font not found: {0:?}")]
596 FontNotFound(FontSelector),
597 #[error("Invalid text input: {0}")]
598 InvalidText(String),
599 #[error("Hyphenation failed: {0}")]
600 HyphenationError(String),
601}
602
603#[derive(Debug, Clone, Copy, PartialEq, Eq)]
605pub enum TextBoundary {
606 Top,
608 Bottom,
610 Start,
612 End,
614}
615
616#[derive(Debug, Clone, Copy, PartialEq, Eq)]
618pub struct CursorBoundsError {
619 pub boundary: TextBoundary,
621 pub cursor: TextCursor,
623}
624
625#[derive(Debug, Clone)]
689pub struct UnifiedConstraints {
690 pub shape_boundaries: Vec<ShapeBoundary>,
692 pub shape_exclusions: Vec<ShapeBoundary>,
693
694 pub available_width: AvailableSpace,
696 pub available_height: Option<f32>,
697
698 pub writing_mode: Option<WritingMode>,
700 pub direction: Option<BidiDirection>,
702 pub text_orientation: TextOrientation,
703 pub text_align: TextAlign,
704 pub text_justify: JustifyContent,
705 pub line_height: f32,
706 pub vertical_align: VerticalAlign,
707
708 pub overflow: OverflowBehavior,
710 pub segment_alignment: SegmentAlignment,
711
712 pub text_combine_upright: Option<TextCombineUpright>,
714 pub exclusion_margin: f32,
715 pub hyphenation: bool,
716 pub hyphenation_language: Option<Language>,
717 pub text_indent: f32,
718 pub initial_letter: Option<InitialLetter>,
719 pub line_clamp: Option<NonZeroUsize>,
720
721 pub text_wrap: TextWrap,
723 pub columns: u32,
724 pub column_gap: f32,
725 pub hanging_punctuation: bool,
726}
727
728impl Default for UnifiedConstraints {
729 fn default() -> Self {
730 Self {
731 shape_boundaries: Vec::new(),
732 shape_exclusions: Vec::new(),
733
734 available_width: AvailableSpace::MaxContent,
741 available_height: None,
742 writing_mode: None,
743 direction: None, text_orientation: TextOrientation::default(),
745 text_align: TextAlign::default(),
746 text_justify: JustifyContent::default(),
747 line_height: 16.0, vertical_align: VerticalAlign::default(),
749 overflow: OverflowBehavior::default(),
750 segment_alignment: SegmentAlignment::default(),
751 text_combine_upright: None,
752 exclusion_margin: 0.0,
753 hyphenation: false,
754 hyphenation_language: None,
755 columns: 1,
756 column_gap: 0.0,
757 hanging_punctuation: false,
758 text_indent: 0.0,
759 initial_letter: None,
760 line_clamp: None,
761 text_wrap: TextWrap::default(),
762 }
763 }
764}
765
766impl Hash for UnifiedConstraints {
768 fn hash<H: Hasher>(&self, state: &mut H) {
769 self.shape_boundaries.hash(state);
770 self.shape_exclusions.hash(state);
771 self.available_width.hash(state);
772 self.available_height
773 .map(|h| h.round() as usize)
774 .hash(state);
775 self.writing_mode.hash(state);
776 self.direction.hash(state);
777 self.text_orientation.hash(state);
778 self.text_align.hash(state);
779 self.text_justify.hash(state);
780 (self.line_height.round() as usize).hash(state);
781 self.vertical_align.hash(state);
782 self.overflow.hash(state);
783 self.text_combine_upright.hash(state);
784 (self.exclusion_margin.round() as usize).hash(state);
785 self.hyphenation.hash(state);
786 self.hyphenation_language.hash(state);
787 self.columns.hash(state);
788 (self.column_gap.round() as usize).hash(state);
789 self.hanging_punctuation.hash(state);
790 }
791}
792
793impl PartialEq for UnifiedConstraints {
794 fn eq(&self, other: &Self) -> bool {
795 self.shape_boundaries == other.shape_boundaries
796 && self.shape_exclusions == other.shape_exclusions
797 && self.available_width == other.available_width
798 && match (self.available_height, other.available_height) {
799 (None, None) => true,
800 (Some(h1), Some(h2)) => round_eq(h1, h2),
801 _ => false,
802 }
803 && self.writing_mode == other.writing_mode
804 && self.direction == other.direction
805 && self.text_orientation == other.text_orientation
806 && self.text_align == other.text_align
807 && self.text_justify == other.text_justify
808 && round_eq(self.line_height, other.line_height)
809 && self.vertical_align == other.vertical_align
810 && self.overflow == other.overflow
811 && self.text_combine_upright == other.text_combine_upright
812 && round_eq(self.exclusion_margin, other.exclusion_margin)
813 && self.hyphenation == other.hyphenation
814 && self.hyphenation_language == other.hyphenation_language
815 && self.columns == other.columns
816 && round_eq(self.column_gap, other.column_gap)
817 && self.hanging_punctuation == other.hanging_punctuation
818 }
819}
820
821impl Eq for UnifiedConstraints {}
822
823impl UnifiedConstraints {
824 fn direction(&self, fallback: BidiDirection) -> BidiDirection {
825 match self.writing_mode {
826 Some(s) => s.get_direction().unwrap_or(fallback),
827 None => fallback,
828 }
829 }
830 fn is_vertical(&self) -> bool {
831 matches!(
832 self.writing_mode,
833 Some(WritingMode::VerticalRl) | Some(WritingMode::VerticalLr)
834 )
835 }
836}
837
838#[derive(Debug, Clone)]
840pub struct LineConstraints {
841 pub segments: Vec<LineSegment>,
842 pub total_available: f32,
843}
844
845impl WritingMode {
846 fn get_direction(&self) -> Option<BidiDirection> {
847 match self {
848 WritingMode::HorizontalTb => None,
850 WritingMode::VerticalRl => Some(BidiDirection::Rtl),
851 WritingMode::VerticalLr => Some(BidiDirection::Ltr),
852 WritingMode::SidewaysRl => Some(BidiDirection::Rtl),
853 WritingMode::SidewaysLr => Some(BidiDirection::Ltr),
854 }
855 }
856}
857
858#[derive(Debug, Clone, Hash)]
860pub struct StyledRun {
861 pub text: String,
862 pub style: Arc<StyleProperties>,
863 pub logical_start_byte: usize,
865 pub source_node_id: Option<NodeId>,
868}
869
870#[derive(Debug, Clone)]
872pub struct VisualRun<'a> {
873 pub text_slice: &'a str,
874 pub style: Arc<StyleProperties>,
875 pub logical_start_byte: usize,
876 pub bidi_level: BidiLevel,
877 pub script: Script,
878 pub language: Language,
879}
880
881#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
886pub struct FontSelector {
887 pub family: String,
888 pub weight: FcWeight,
889 pub style: FontStyle,
890 pub unicode_ranges: Vec<UnicodeRange>,
891}
892
893impl Default for FontSelector {
894 fn default() -> Self {
895 Self {
896 family: "serif".to_string(),
897 weight: FcWeight::Normal,
898 style: FontStyle::Normal,
899 unicode_ranges: Vec::new(),
900 }
901 }
902}
903
904#[derive(Debug, Clone)]
911pub enum FontStack {
912 Stack(Vec<FontSelector>),
915 Ref(azul_css::props::basic::font::FontRef),
918}
919
920impl Default for FontStack {
921 fn default() -> Self {
922 FontStack::Stack(vec![FontSelector::default()])
923 }
924}
925
926impl FontStack {
927 pub fn is_ref(&self) -> bool {
929 matches!(self, FontStack::Ref(_))
930 }
931
932 pub fn as_ref(&self) -> Option<&azul_css::props::basic::font::FontRef> {
934 match self {
935 FontStack::Ref(r) => Some(r),
936 _ => None,
937 }
938 }
939
940 pub fn as_stack(&self) -> Option<&[FontSelector]> {
942 match self {
943 FontStack::Stack(s) => Some(s),
944 _ => None,
945 }
946 }
947
948 pub fn first_selector(&self) -> Option<&FontSelector> {
950 match self {
951 FontStack::Stack(s) => s.first(),
952 FontStack::Ref(_) => None,
953 }
954 }
955
956 pub fn first_family(&self) -> &str {
958 match self {
959 FontStack::Stack(s) => s.first().map(|f| f.family.as_str()).unwrap_or("serif"),
960 FontStack::Ref(_) => "<embedded-font>",
961 }
962 }
963}
964
965impl PartialEq for FontStack {
966 fn eq(&self, other: &Self) -> bool {
967 match (self, other) {
968 (FontStack::Stack(a), FontStack::Stack(b)) => a == b,
969 (FontStack::Ref(a), FontStack::Ref(b)) => a.parsed == b.parsed,
970 _ => false,
971 }
972 }
973}
974
975impl Eq for FontStack {}
976
977impl Hash for FontStack {
978 fn hash<H: Hasher>(&self, state: &mut H) {
979 core::mem::discriminant(self).hash(state);
980 match self {
981 FontStack::Stack(s) => s.hash(state),
982 FontStack::Ref(r) => (r.parsed as usize).hash(state),
983 }
984 }
985}
986
987#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
991pub struct FontHash {
992 pub font_hash: u64,
994}
995
996impl FontHash {
997 pub fn invalid() -> Self {
998 Self { font_hash: 0 }
999 }
1000
1001 pub fn from_hash(font_hash: u64) -> Self {
1002 Self { font_hash }
1003 }
1004}
1005
1006#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1007pub enum FontStyle {
1008 Normal,
1009 Italic,
1010 Oblique,
1011}
1012
1013#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1015pub enum SegmentAlignment {
1016 #[default]
1018 First,
1019 Total,
1022}
1023
1024#[derive(Debug, Clone)]
1025pub struct VerticalMetrics {
1026 pub advance: f32,
1027 pub bearing_x: f32,
1028 pub bearing_y: f32,
1029 pub origin_y: f32,
1030}
1031
1032#[derive(Debug, Clone)]
1035pub struct LayoutFontMetrics {
1036 pub ascent: f32,
1037 pub descent: f32,
1038 pub line_gap: f32,
1039 pub units_per_em: u16,
1040}
1041
1042impl LayoutFontMetrics {
1043 pub fn baseline_scaled(&self, font_size: f32) -> f32 {
1044 let scale = font_size / self.units_per_em as f32;
1045 self.ascent * scale
1046 }
1047
1048 pub fn from_font_metrics(metrics: &azul_css::props::basic::FontMetrics) -> Self {
1050 Self {
1051 ascent: metrics.ascender as f32,
1052 descent: metrics.descender as f32,
1053 line_gap: metrics.line_gap as f32,
1054 units_per_em: metrics.units_per_em,
1055 }
1056 }
1057}
1058
1059#[derive(Debug, Clone)]
1060pub struct LineSegment {
1061 pub start_x: f32,
1062 pub width: f32,
1063 pub priority: u8,
1065}
1066
1067#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
1068pub enum TextWrap {
1069 #[default]
1070 Wrap,
1071 Balance,
1072 NoWrap,
1073}
1074
1075#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
1077pub struct InitialLetter {
1078 pub size: f32,
1080 pub sink: u32,
1082 pub count: NonZeroUsize,
1084}
1085
1086impl Eq for InitialLetter {}
1091
1092impl Hash for InitialLetter {
1093 fn hash<H: Hasher>(&self, state: &mut H) {
1094 (self.size.round() as usize).hash(state);
1099 self.sink.hash(state);
1100 self.count.hash(state);
1101 }
1102}
1103
1104#[derive(Debug, Clone, PartialOrd)]
1106pub enum PathSegment {
1107 MoveTo(Point),
1108 LineTo(Point),
1109 CurveTo {
1110 control1: Point,
1111 control2: Point,
1112 end: Point,
1113 },
1114 QuadTo {
1115 control: Point,
1116 end: Point,
1117 },
1118 Arc {
1119 center: Point,
1120 radius: f32,
1121 start_angle: f32,
1122 end_angle: f32,
1123 },
1124 Close,
1125}
1126
1127impl Hash for PathSegment {
1129 fn hash<H: Hasher>(&self, state: &mut H) {
1130 discriminant(self).hash(state);
1132
1133 match self {
1134 PathSegment::MoveTo(p) => p.hash(state),
1135 PathSegment::LineTo(p) => p.hash(state),
1136 PathSegment::CurveTo {
1137 control1,
1138 control2,
1139 end,
1140 } => {
1141 control1.hash(state);
1142 control2.hash(state);
1143 end.hash(state);
1144 }
1145 PathSegment::QuadTo { control, end } => {
1146 control.hash(state);
1147 end.hash(state);
1148 }
1149 PathSegment::Arc {
1150 center,
1151 radius,
1152 start_angle,
1153 end_angle,
1154 } => {
1155 center.hash(state);
1156 (radius.round() as usize).hash(state);
1157 (start_angle.round() as usize).hash(state);
1158 (end_angle.round() as usize).hash(state);
1159 }
1160 PathSegment::Close => {} }
1162 }
1163}
1164
1165impl PartialEq for PathSegment {
1166 fn eq(&self, other: &Self) -> bool {
1167 match (self, other) {
1168 (PathSegment::MoveTo(a), PathSegment::MoveTo(b)) => a == b,
1169 (PathSegment::LineTo(a), PathSegment::LineTo(b)) => a == b,
1170 (
1171 PathSegment::CurveTo {
1172 control1: c1a,
1173 control2: c2a,
1174 end: ea,
1175 },
1176 PathSegment::CurveTo {
1177 control1: c1b,
1178 control2: c2b,
1179 end: eb,
1180 },
1181 ) => c1a == c1b && c2a == c2b && ea == eb,
1182 (
1183 PathSegment::QuadTo {
1184 control: ca,
1185 end: ea,
1186 },
1187 PathSegment::QuadTo {
1188 control: cb,
1189 end: eb,
1190 },
1191 ) => ca == cb && ea == eb,
1192 (
1193 PathSegment::Arc {
1194 center: ca,
1195 radius: ra,
1196 start_angle: sa_a,
1197 end_angle: ea_a,
1198 },
1199 PathSegment::Arc {
1200 center: cb,
1201 radius: rb,
1202 start_angle: sa_b,
1203 end_angle: ea_b,
1204 },
1205 ) => ca == cb && round_eq(*ra, *rb) && round_eq(*sa_a, *sa_b) && round_eq(*ea_a, *ea_b),
1206 (PathSegment::Close, PathSegment::Close) => true,
1207 _ => false, }
1209 }
1210}
1211
1212impl Eq for PathSegment {}
1213
1214#[derive(Debug, Clone, Hash)]
1216pub enum InlineContent {
1217 Text(StyledRun),
1218 Image(InlineImage),
1219 Shape(InlineShape),
1220 Space(InlineSpace),
1221 LineBreak(InlineBreak),
1222 Tab {
1224 style: Arc<StyleProperties>,
1225 },
1226 Marker {
1230 run: StyledRun,
1231 position_outside: bool,
1233 },
1234 Ruby {
1236 base: Vec<InlineContent>,
1237 text: Vec<InlineContent>,
1238 style: Arc<StyleProperties>,
1240 },
1241}
1242
1243#[derive(Debug, Clone)]
1244pub struct InlineImage {
1245 pub source: ImageSource,
1246 pub intrinsic_size: Size,
1247 pub display_size: Option<Size>,
1248 pub baseline_offset: f32,
1250 pub alignment: VerticalAlign,
1251 pub object_fit: ObjectFit,
1252}
1253
1254impl PartialEq for InlineImage {
1255 fn eq(&self, other: &Self) -> bool {
1256 self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
1257 && self.source == other.source
1258 && self.intrinsic_size == other.intrinsic_size
1259 && self.display_size == other.display_size
1260 && self.alignment == other.alignment
1261 && self.object_fit == other.object_fit
1262 }
1263}
1264
1265impl Eq for InlineImage {}
1266
1267impl Hash for InlineImage {
1268 fn hash<H: Hasher>(&self, state: &mut H) {
1269 self.source.hash(state);
1270 self.intrinsic_size.hash(state);
1271 self.display_size.hash(state);
1272 self.baseline_offset.to_bits().hash(state);
1273 self.alignment.hash(state);
1274 self.object_fit.hash(state);
1275 }
1276}
1277
1278impl PartialOrd for InlineImage {
1279 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1280 Some(self.cmp(other))
1281 }
1282}
1283
1284impl Ord for InlineImage {
1285 fn cmp(&self, other: &Self) -> Ordering {
1286 self.source
1287 .cmp(&other.source)
1288 .then_with(|| self.intrinsic_size.cmp(&other.intrinsic_size))
1289 .then_with(|| self.display_size.cmp(&other.display_size))
1290 .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
1291 .then_with(|| self.alignment.cmp(&other.alignment))
1292 .then_with(|| self.object_fit.cmp(&other.object_fit))
1293 }
1294}
1295
1296#[derive(Debug, Clone)]
1298pub struct Glyph {
1299 pub glyph_id: u16,
1301 pub codepoint: char,
1302 pub font_hash: u64,
1304 pub font_metrics: LayoutFontMetrics,
1306 pub style: Arc<StyleProperties>,
1307 pub source: GlyphSource,
1308
1309 pub logical_byte_index: usize,
1311 pub logical_byte_len: usize,
1312 pub content_index: usize,
1313 pub cluster: u32,
1314
1315 pub advance: f32,
1317 pub kerning: f32,
1318 pub offset: Point,
1319
1320 pub vertical_advance: f32,
1322 pub vertical_origin_y: f32, pub vertical_bearing: Point,
1324 pub orientation: GlyphOrientation,
1325
1326 pub script: Script,
1328 pub bidi_level: BidiLevel,
1329}
1330
1331impl Glyph {
1332 #[inline]
1333 fn bounds(&self) -> Rect {
1334 Rect {
1335 x: 0.0,
1336 y: 0.0,
1337 width: self.advance,
1338 height: self.style.line_height,
1339 }
1340 }
1341
1342 #[inline]
1343 fn character_class(&self) -> CharacterClass {
1344 classify_character(self.codepoint as u32)
1345 }
1346
1347 #[inline]
1348 fn is_whitespace(&self) -> bool {
1349 self.character_class() == CharacterClass::Space
1350 }
1351
1352 #[inline]
1353 fn can_justify(&self) -> bool {
1354 !self.codepoint.is_whitespace() && self.character_class() != CharacterClass::Combining
1355 }
1356
1357 #[inline]
1358 fn justification_priority(&self) -> u8 {
1359 get_justification_priority(self.character_class())
1360 }
1361
1362 #[inline]
1363 fn break_opportunity_after(&self) -> bool {
1364 let is_whitespace = self.codepoint.is_whitespace();
1365 let is_soft_hyphen = self.codepoint == '\u{00AD}';
1366 is_whitespace || is_soft_hyphen
1367 }
1368}
1369
1370#[derive(Debug, Clone)]
1372pub struct TextRunInfo<'a> {
1373 pub text: &'a str,
1374 pub style: Arc<StyleProperties>,
1375 pub logical_start: usize,
1376 pub content_index: usize,
1377}
1378
1379#[derive(Debug, Clone)]
1380pub enum ImageSource {
1381 Ref(ImageRef),
1383 Url(String),
1385 Data(Arc<[u8]>),
1387 Svg(Arc<str>),
1389 Placeholder(Size),
1391}
1392
1393impl PartialEq for ImageSource {
1394 fn eq(&self, other: &Self) -> bool {
1395 match (self, other) {
1396 (ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash() == b.get_hash(),
1397 (ImageSource::Url(a), ImageSource::Url(b)) => a == b,
1398 (ImageSource::Data(a), ImageSource::Data(b)) => Arc::ptr_eq(a, b),
1399 (ImageSource::Svg(a), ImageSource::Svg(b)) => Arc::ptr_eq(a, b),
1400 (ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
1401 a.width.to_bits() == b.width.to_bits() && a.height.to_bits() == b.height.to_bits()
1402 }
1403 _ => false,
1404 }
1405 }
1406}
1407
1408impl Eq for ImageSource {}
1409
1410impl std::hash::Hash for ImageSource {
1411 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1412 core::mem::discriminant(self).hash(state);
1413 match self {
1414 ImageSource::Ref(r) => r.get_hash().hash(state),
1415 ImageSource::Url(s) => s.hash(state),
1416 ImageSource::Data(d) => (Arc::as_ptr(d) as *const u8 as usize).hash(state),
1417 ImageSource::Svg(s) => (Arc::as_ptr(s) as *const u8 as usize).hash(state),
1418 ImageSource::Placeholder(sz) => {
1419 sz.width.to_bits().hash(state);
1420 sz.height.to_bits().hash(state);
1421 }
1422 }
1423 }
1424}
1425
1426impl PartialOrd for ImageSource {
1427 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1428 Some(self.cmp(other))
1429 }
1430}
1431
1432impl Ord for ImageSource {
1433 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1434 fn variant_index(s: &ImageSource) -> u8 {
1435 match s {
1436 ImageSource::Ref(_) => 0,
1437 ImageSource::Url(_) => 1,
1438 ImageSource::Data(_) => 2,
1439 ImageSource::Svg(_) => 3,
1440 ImageSource::Placeholder(_) => 4,
1441 }
1442 }
1443 match (self, other) {
1444 (ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash().cmp(&b.get_hash()),
1445 (ImageSource::Url(a), ImageSource::Url(b)) => a.cmp(b),
1446 (ImageSource::Data(a), ImageSource::Data(b)) => {
1447 (Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
1448 }
1449 (ImageSource::Svg(a), ImageSource::Svg(b)) => {
1450 (Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
1451 }
1452 (ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
1453 (a.width.to_bits(), a.height.to_bits())
1454 .cmp(&(b.width.to_bits(), b.height.to_bits()))
1455 }
1456 _ => variant_index(self).cmp(&variant_index(other)),
1458 }
1459 }
1460}
1461
1462#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
1463pub enum VerticalAlign {
1464 #[default]
1466 Baseline,
1467 Bottom,
1469 Top,
1471 Middle,
1473 TextTop,
1475 TextBottom,
1477 Sub,
1479 Super,
1481 Offset(f32),
1483}
1484
1485impl std::hash::Hash for VerticalAlign {
1486 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1487 core::mem::discriminant(self).hash(state);
1488 if let VerticalAlign::Offset(f) = self {
1489 f.to_bits().hash(state);
1490 }
1491 }
1492}
1493
1494impl Eq for VerticalAlign {}
1495
1496impl Ord for VerticalAlign {
1497 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1498 self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
1499 }
1500}
1501
1502#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
1503pub enum ObjectFit {
1504 Fill,
1506 Contain,
1508 Cover,
1510 None,
1512 ScaleDown,
1514}
1515
1516#[derive(Debug, Clone, PartialEq)]
1522pub struct InlineBorderInfo {
1523 pub top: f32,
1525 pub right: f32,
1526 pub bottom: f32,
1527 pub left: f32,
1528 pub top_color: ColorU,
1530 pub right_color: ColorU,
1531 pub bottom_color: ColorU,
1532 pub left_color: ColorU,
1533 pub radius: Option<f32>,
1535 pub padding_top: f32,
1537 pub padding_right: f32,
1538 pub padding_bottom: f32,
1539 pub padding_left: f32,
1540}
1541
1542impl Default for InlineBorderInfo {
1543 fn default() -> Self {
1544 Self {
1545 top: 0.0,
1546 right: 0.0,
1547 bottom: 0.0,
1548 left: 0.0,
1549 top_color: ColorU::TRANSPARENT,
1550 right_color: ColorU::TRANSPARENT,
1551 bottom_color: ColorU::TRANSPARENT,
1552 left_color: ColorU::TRANSPARENT,
1553 radius: None,
1554 padding_top: 0.0,
1555 padding_right: 0.0,
1556 padding_bottom: 0.0,
1557 padding_left: 0.0,
1558 }
1559 }
1560}
1561
1562impl InlineBorderInfo {
1563 pub fn has_border(&self) -> bool {
1565 self.top > 0.0 || self.right > 0.0 || self.bottom > 0.0 || self.left > 0.0
1566 }
1567
1568 pub fn has_chrome(&self) -> bool {
1570 self.has_border()
1571 || self.padding_top > 0.0
1572 || self.padding_right > 0.0
1573 || self.padding_bottom > 0.0
1574 || self.padding_left > 0.0
1575 }
1576
1577 pub fn left_inset(&self) -> f32 { self.left + self.padding_left }
1579 pub fn right_inset(&self) -> f32 { self.right + self.padding_right }
1581 pub fn top_inset(&self) -> f32 { self.top + self.padding_top }
1583 pub fn bottom_inset(&self) -> f32 { self.bottom + self.padding_bottom }
1585}
1586
1587#[derive(Debug, Clone)]
1588pub struct InlineShape {
1589 pub shape_def: ShapeDefinition,
1590 pub fill: Option<ColorU>,
1591 pub stroke: Option<Stroke>,
1592 pub baseline_offset: f32,
1593 pub alignment: VerticalAlign,
1596 pub source_node_id: Option<azul_core::dom::NodeId>,
1600}
1601
1602#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1603pub enum OverflowBehavior {
1604 Visible,
1606 Hidden,
1608 Scroll,
1610 #[default]
1612 Auto,
1613 Break,
1615}
1616
1617#[derive(Debug, Clone)]
1618pub struct MeasuredImage {
1619 pub source: ImageSource,
1620 pub size: Size,
1621 pub baseline_offset: f32,
1622 pub alignment: VerticalAlign,
1623 pub content_index: usize,
1624}
1625
1626#[derive(Debug, Clone)]
1627pub struct MeasuredShape {
1628 pub shape_def: ShapeDefinition,
1629 pub size: Size,
1630 pub baseline_offset: f32,
1631 pub alignment: VerticalAlign,
1632 pub content_index: usize,
1633}
1634
1635#[derive(Debug, Clone)]
1636pub struct InlineSpace {
1637 pub width: f32,
1638 pub is_breaking: bool, pub is_stretchy: bool, }
1641
1642impl PartialEq for InlineSpace {
1643 fn eq(&self, other: &Self) -> bool {
1644 self.width.to_bits() == other.width.to_bits()
1645 && self.is_breaking == other.is_breaking
1646 && self.is_stretchy == other.is_stretchy
1647 }
1648}
1649
1650impl Eq for InlineSpace {}
1651
1652impl Hash for InlineSpace {
1653 fn hash<H: Hasher>(&self, state: &mut H) {
1654 self.width.to_bits().hash(state);
1655 self.is_breaking.hash(state);
1656 self.is_stretchy.hash(state);
1657 }
1658}
1659
1660impl PartialOrd for InlineSpace {
1661 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1662 Some(self.cmp(other))
1663 }
1664}
1665
1666impl Ord for InlineSpace {
1667 fn cmp(&self, other: &Self) -> Ordering {
1668 self.width
1669 .total_cmp(&other.width)
1670 .then_with(|| self.is_breaking.cmp(&other.is_breaking))
1671 .then_with(|| self.is_stretchy.cmp(&other.is_stretchy))
1672 }
1673}
1674
1675impl PartialEq for InlineShape {
1676 fn eq(&self, other: &Self) -> bool {
1677 self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
1678 && self.shape_def == other.shape_def
1679 && self.fill == other.fill
1680 && self.stroke == other.stroke
1681 && self.alignment == other.alignment
1682 && self.source_node_id == other.source_node_id
1683 }
1684}
1685
1686impl Eq for InlineShape {}
1687
1688impl Hash for InlineShape {
1689 fn hash<H: Hasher>(&self, state: &mut H) {
1690 self.shape_def.hash(state);
1691 self.fill.hash(state);
1692 self.stroke.hash(state);
1693 self.baseline_offset.to_bits().hash(state);
1694 self.alignment.hash(state);
1695 self.source_node_id.hash(state);
1696 }
1697}
1698
1699impl PartialOrd for InlineShape {
1700 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1701 Some(
1702 self.shape_def
1703 .partial_cmp(&other.shape_def)?
1704 .then_with(|| self.fill.cmp(&other.fill))
1705 .then_with(|| {
1706 self.stroke
1707 .partial_cmp(&other.stroke)
1708 .unwrap_or(Ordering::Equal)
1709 })
1710 .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
1711 .then_with(|| self.alignment.cmp(&other.alignment))
1712 .then_with(|| self.source_node_id.cmp(&other.source_node_id)),
1713 )
1714 }
1715}
1716
1717#[derive(Debug, Default, Clone, Copy)]
1718pub struct Rect {
1719 pub x: f32,
1720 pub y: f32,
1721 pub width: f32,
1722 pub height: f32,
1723}
1724
1725impl PartialEq for Rect {
1726 fn eq(&self, other: &Self) -> bool {
1727 round_eq(self.x, other.x)
1728 && round_eq(self.y, other.y)
1729 && round_eq(self.width, other.width)
1730 && round_eq(self.height, other.height)
1731 }
1732}
1733impl Eq for Rect {}
1734
1735impl Hash for Rect {
1736 fn hash<H: Hasher>(&self, state: &mut H) {
1737 (self.x.round() as usize).hash(state);
1740 (self.y.round() as usize).hash(state);
1741 (self.width.round() as usize).hash(state);
1742 (self.height.round() as usize).hash(state);
1743 }
1744}
1745
1746#[derive(Debug, Default, Clone, Copy, PartialOrd)]
1747pub struct Size {
1748 pub width: f32,
1749 pub height: f32,
1750}
1751
1752impl Ord for Size {
1753 fn cmp(&self, other: &Self) -> Ordering {
1754 (self.width.round() as usize)
1755 .cmp(&(other.width.round() as usize))
1756 .then_with(|| (self.height.round() as usize).cmp(&(other.height.round() as usize)))
1757 }
1758}
1759
1760impl Hash for Size {
1762 fn hash<H: Hasher>(&self, state: &mut H) {
1763 (self.width.round() as usize).hash(state);
1764 (self.height.round() as usize).hash(state);
1765 }
1766}
1767impl PartialEq for Size {
1768 fn eq(&self, other: &Self) -> bool {
1769 round_eq(self.width, other.width) && round_eq(self.height, other.height)
1770 }
1771}
1772impl Eq for Size {}
1773
1774impl Size {
1775 pub const fn zero() -> Self {
1776 Self::new(0.0, 0.0)
1777 }
1778 pub const fn new(width: f32, height: f32) -> Self {
1779 Self { width, height }
1780 }
1781}
1782
1783#[derive(Debug, Default, Clone, Copy, PartialOrd)]
1784pub struct Point {
1785 pub x: f32,
1786 pub y: f32,
1787}
1788
1789impl Hash for Point {
1791 fn hash<H: Hasher>(&self, state: &mut H) {
1792 (self.x.round() as usize).hash(state);
1793 (self.y.round() as usize).hash(state);
1794 }
1795}
1796
1797impl PartialEq for Point {
1798 fn eq(&self, other: &Self) -> bool {
1799 round_eq(self.x, other.x) && round_eq(self.y, other.y)
1800 }
1801}
1802
1803impl Eq for Point {}
1804
1805#[derive(Debug, Clone, PartialOrd)]
1806pub enum ShapeDefinition {
1807 Rectangle {
1808 size: Size,
1809 corner_radius: Option<f32>,
1810 },
1811 Circle {
1812 radius: f32,
1813 },
1814 Ellipse {
1815 radii: Size,
1816 },
1817 Polygon {
1818 points: Vec<Point>,
1819 },
1820 Path {
1821 segments: Vec<PathSegment>,
1822 },
1823}
1824
1825impl Hash for ShapeDefinition {
1827 fn hash<H: Hasher>(&self, state: &mut H) {
1828 discriminant(self).hash(state);
1829 match self {
1830 ShapeDefinition::Rectangle {
1831 size,
1832 corner_radius,
1833 } => {
1834 size.hash(state);
1835 corner_radius.map(|r| r.round() as usize).hash(state);
1836 }
1837 ShapeDefinition::Circle { radius } => {
1838 (radius.round() as usize).hash(state);
1839 }
1840 ShapeDefinition::Ellipse { radii } => {
1841 radii.hash(state);
1842 }
1843 ShapeDefinition::Polygon { points } => {
1844 points.hash(state);
1846 }
1847 ShapeDefinition::Path { segments } => {
1848 segments.hash(state);
1850 }
1851 }
1852 }
1853}
1854
1855impl PartialEq for ShapeDefinition {
1856 fn eq(&self, other: &Self) -> bool {
1857 match (self, other) {
1858 (
1859 ShapeDefinition::Rectangle {
1860 size: s1,
1861 corner_radius: r1,
1862 },
1863 ShapeDefinition::Rectangle {
1864 size: s2,
1865 corner_radius: r2,
1866 },
1867 ) => {
1868 s1 == s2
1869 && match (r1, r2) {
1870 (None, None) => true,
1871 (Some(v1), Some(v2)) => round_eq(*v1, *v2),
1872 _ => false,
1873 }
1874 }
1875 (ShapeDefinition::Circle { radius: r1 }, ShapeDefinition::Circle { radius: r2 }) => {
1876 round_eq(*r1, *r2)
1877 }
1878 (ShapeDefinition::Ellipse { radii: r1 }, ShapeDefinition::Ellipse { radii: r2 }) => {
1879 r1 == r2
1880 }
1881 (ShapeDefinition::Polygon { points: p1 }, ShapeDefinition::Polygon { points: p2 }) => {
1882 p1 == p2
1883 }
1884 (ShapeDefinition::Path { segments: s1 }, ShapeDefinition::Path { segments: s2 }) => {
1885 s1 == s2
1886 }
1887 _ => false,
1888 }
1889 }
1890}
1891impl Eq for ShapeDefinition {}
1892
1893impl ShapeDefinition {
1894 pub fn get_size(&self) -> Size {
1896 match self {
1897 ShapeDefinition::Rectangle { size, .. } => *size,
1899
1900 ShapeDefinition::Circle { radius } => {
1902 let diameter = radius * 2.0;
1903 Size::new(diameter, diameter)
1904 }
1905
1906 ShapeDefinition::Ellipse { radii } => Size::new(radii.width * 2.0, radii.height * 2.0),
1908
1909 ShapeDefinition::Polygon { points } => calculate_bounding_box_size(points),
1911
1912 ShapeDefinition::Path { segments } => {
1919 let mut points = Vec::new();
1920 let mut current_pos = Point { x: 0.0, y: 0.0 };
1921
1922 for segment in segments {
1923 match segment {
1924 PathSegment::MoveTo(p) | PathSegment::LineTo(p) => {
1925 points.push(*p);
1926 current_pos = *p;
1927 }
1928 PathSegment::QuadTo { control, end } => {
1929 points.push(current_pos);
1930 points.push(*control);
1931 points.push(*end);
1932 current_pos = *end;
1933 }
1934 PathSegment::CurveTo {
1935 control1,
1936 control2,
1937 end,
1938 } => {
1939 points.push(current_pos);
1940 points.push(*control1);
1941 points.push(*control2);
1942 points.push(*end);
1943 current_pos = *end;
1944 }
1945 PathSegment::Arc {
1946 center,
1947 radius,
1948 start_angle,
1949 end_angle,
1950 } => {
1951 let start_point = Point {
1953 x: center.x + radius * start_angle.cos(),
1954 y: center.y + radius * start_angle.sin(),
1955 };
1956 let end_point = Point {
1957 x: center.x + radius * end_angle.cos(),
1958 y: center.y + radius * end_angle.sin(),
1959 };
1960 points.push(start_point);
1961 points.push(end_point);
1962
1963 let mut normalized_end = *end_angle;
1967 while normalized_end < *start_angle {
1968 normalized_end += 2.0 * std::f32::consts::PI;
1969 }
1970
1971 let mut check_angle = (*start_angle / std::f32::consts::FRAC_PI_2)
1974 .ceil()
1975 * std::f32::consts::FRAC_PI_2;
1976
1977 while check_angle < normalized_end {
1981 points.push(Point {
1982 x: center.x + radius * check_angle.cos(),
1983 y: center.y + radius * check_angle.sin(),
1984 });
1985 check_angle += std::f32::consts::FRAC_PI_2;
1986 }
1987
1988 current_pos = end_point;
1991 }
1992 PathSegment::Close => {
1993 }
1995 }
1996 }
1997 calculate_bounding_box_size(&points)
1998 }
1999 }
2000 }
2001}
2002
2003fn calculate_bounding_box_size(points: &[Point]) -> Size {
2005 if points.is_empty() {
2006 return Size::zero();
2007 }
2008
2009 let mut min_x = f32::MAX;
2010 let mut max_x = f32::MIN;
2011 let mut min_y = f32::MAX;
2012 let mut max_y = f32::MIN;
2013
2014 for point in points {
2015 min_x = min_x.min(point.x);
2016 max_x = max_x.max(point.x);
2017 min_y = min_y.min(point.y);
2018 max_y = max_y.max(point.y);
2019 }
2020
2021 if min_x > max_x || min_y > max_y {
2023 return Size::zero();
2024 }
2025
2026 Size::new(max_x - min_x, max_y - min_y)
2027}
2028
2029#[derive(Debug, Clone, PartialOrd)]
2030pub struct Stroke {
2031 pub color: ColorU,
2032 pub width: f32,
2033 pub dash_pattern: Option<Vec<f32>>,
2034}
2035
2036impl Hash for Stroke {
2038 fn hash<H: Hasher>(&self, state: &mut H) {
2039 self.color.hash(state);
2040 (self.width.round() as usize).hash(state);
2041
2042 match &self.dash_pattern {
2044 None => 0u8.hash(state), Some(pattern) => {
2046 1u8.hash(state); pattern.len().hash(state); for &val in pattern {
2049 (val.round() as usize).hash(state); }
2051 }
2052 }
2053 }
2054}
2055
2056impl PartialEq for Stroke {
2057 fn eq(&self, other: &Self) -> bool {
2058 if self.color != other.color || !round_eq(self.width, other.width) {
2059 return false;
2060 }
2061 match (&self.dash_pattern, &other.dash_pattern) {
2062 (None, None) => true,
2063 (Some(p1), Some(p2)) => {
2064 p1.len() == p2.len() && p1.iter().zip(p2.iter()).all(|(a, b)| round_eq(*a, *b))
2065 }
2066 _ => false,
2067 }
2068 }
2069}
2070
2071impl Eq for Stroke {}
2072
2073fn round_eq(a: f32, b: f32) -> bool {
2075 (a.round() as isize) == (b.round() as isize)
2076}
2077
2078#[derive(Debug, Clone)]
2079pub enum ShapeBoundary {
2080 Rectangle(Rect),
2081 Circle { center: Point, radius: f32 },
2082 Ellipse { center: Point, radii: Size },
2083 Polygon { points: Vec<Point> },
2084 Path { segments: Vec<PathSegment> },
2085}
2086
2087impl ShapeBoundary {
2088 pub fn inflate(&self, margin: f32) -> Self {
2089 if margin == 0.0 {
2090 return self.clone();
2091 }
2092 match self {
2093 Self::Rectangle(rect) => Self::Rectangle(Rect {
2094 x: rect.x - margin,
2095 y: rect.y - margin,
2096 width: (rect.width + margin * 2.0).max(0.0),
2097 height: (rect.height + margin * 2.0).max(0.0),
2098 }),
2099 Self::Circle { center, radius } => Self::Circle {
2100 center: *center,
2101 radius: radius + margin,
2102 },
2103 _ => self.clone(),
2106 }
2107 }
2108}
2109
2110impl Hash for ShapeBoundary {
2112 fn hash<H: Hasher>(&self, state: &mut H) {
2113 discriminant(self).hash(state);
2114 match self {
2115 ShapeBoundary::Rectangle(rect) => rect.hash(state),
2116 ShapeBoundary::Circle { center, radius } => {
2117 center.hash(state);
2118 (radius.round() as usize).hash(state);
2119 }
2120 ShapeBoundary::Ellipse { center, radii } => {
2121 center.hash(state);
2122 radii.hash(state);
2123 }
2124 ShapeBoundary::Polygon { points } => points.hash(state),
2125 ShapeBoundary::Path { segments } => segments.hash(state),
2126 }
2127 }
2128}
2129impl PartialEq for ShapeBoundary {
2130 fn eq(&self, other: &Self) -> bool {
2131 match (self, other) {
2132 (ShapeBoundary::Rectangle(r1), ShapeBoundary::Rectangle(r2)) => r1 == r2,
2133 (
2134 ShapeBoundary::Circle {
2135 center: c1,
2136 radius: r1,
2137 },
2138 ShapeBoundary::Circle {
2139 center: c2,
2140 radius: r2,
2141 },
2142 ) => c1 == c2 && round_eq(*r1, *r2),
2143 (
2144 ShapeBoundary::Ellipse {
2145 center: c1,
2146 radii: r1,
2147 },
2148 ShapeBoundary::Ellipse {
2149 center: c2,
2150 radii: r2,
2151 },
2152 ) => c1 == c2 && r1 == r2,
2153 (ShapeBoundary::Polygon { points: p1 }, ShapeBoundary::Polygon { points: p2 }) => {
2154 p1 == p2
2155 }
2156 (ShapeBoundary::Path { segments: s1 }, ShapeBoundary::Path { segments: s2 }) => {
2157 s1 == s2
2158 }
2159 _ => false,
2160 }
2161 }
2162}
2163impl Eq for ShapeBoundary {}
2164
2165impl ShapeBoundary {
2166 pub fn from_css_shape(
2175 css_shape: &azul_css::shape::CssShape,
2176 reference_box: Rect,
2177 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2178 ) -> Self {
2179 use azul_css::shape::CssShape;
2180
2181 if let Some(msgs) = debug_messages {
2182 msgs.push(LayoutDebugMessage::info(format!(
2183 "[ShapeBoundary::from_css_shape] Input CSS shape: {:?}",
2184 css_shape
2185 )));
2186 msgs.push(LayoutDebugMessage::info(format!(
2187 "[ShapeBoundary::from_css_shape] Reference box: {:?}",
2188 reference_box
2189 )));
2190 }
2191
2192 let result = match css_shape {
2193 CssShape::Circle(circle) => {
2194 let center = Point {
2195 x: reference_box.x + circle.center.x,
2196 y: reference_box.y + circle.center.y,
2197 };
2198 if let Some(msgs) = debug_messages {
2199 msgs.push(LayoutDebugMessage::info(format!(
2200 "[ShapeBoundary::from_css_shape] Circle - CSS center: ({}, {}), radius: {}",
2201 circle.center.x, circle.center.y, circle.radius
2202 )));
2203 msgs.push(LayoutDebugMessage::info(format!(
2204 "[ShapeBoundary::from_css_shape] Circle - Absolute center: ({}, {}), \
2205 radius: {}",
2206 center.x, center.y, circle.radius
2207 )));
2208 }
2209 ShapeBoundary::Circle {
2210 center,
2211 radius: circle.radius,
2212 }
2213 }
2214
2215 CssShape::Ellipse(ellipse) => {
2216 let center = Point {
2217 x: reference_box.x + ellipse.center.x,
2218 y: reference_box.y + ellipse.center.y,
2219 };
2220 let radii = Size {
2221 width: ellipse.radius_x,
2222 height: ellipse.radius_y,
2223 };
2224 if let Some(msgs) = debug_messages {
2225 msgs.push(LayoutDebugMessage::info(format!(
2226 "[ShapeBoundary::from_css_shape] Ellipse - center: ({}, {}), radii: ({}, \
2227 {})",
2228 center.x, center.y, radii.width, radii.height
2229 )));
2230 }
2231 ShapeBoundary::Ellipse { center, radii }
2232 }
2233
2234 CssShape::Polygon(polygon) => {
2235 let points = polygon
2236 .points
2237 .as_ref()
2238 .iter()
2239 .map(|pt| Point {
2240 x: reference_box.x + pt.x,
2241 y: reference_box.y + pt.y,
2242 })
2243 .collect();
2244 if let Some(msgs) = debug_messages {
2245 msgs.push(LayoutDebugMessage::info(format!(
2246 "[ShapeBoundary::from_css_shape] Polygon - {} points",
2247 polygon.points.as_ref().len()
2248 )));
2249 }
2250 ShapeBoundary::Polygon { points }
2251 }
2252
2253 CssShape::Inset(inset) => {
2254 let x = reference_box.x + inset.inset_left;
2256 let y = reference_box.y + inset.inset_top;
2257 let width = reference_box.width - inset.inset_left - inset.inset_right;
2258 let height = reference_box.height - inset.inset_top - inset.inset_bottom;
2259
2260 if let Some(msgs) = debug_messages {
2261 msgs.push(LayoutDebugMessage::info(format!(
2262 "[ShapeBoundary::from_css_shape] Inset - insets: ({}, {}, {}, {})",
2263 inset.inset_top, inset.inset_right, inset.inset_bottom, inset.inset_left
2264 )));
2265 msgs.push(LayoutDebugMessage::info(format!(
2266 "[ShapeBoundary::from_css_shape] Inset - resulting rect: x={}, y={}, \
2267 w={}, h={}",
2268 x, y, width, height
2269 )));
2270 }
2271
2272 ShapeBoundary::Rectangle(Rect {
2273 x,
2274 y,
2275 width: width.max(0.0),
2276 height: height.max(0.0),
2277 })
2278 }
2279
2280 CssShape::Path(path) => {
2281 if let Some(msgs) = debug_messages {
2282 msgs.push(LayoutDebugMessage::info(
2283 "[ShapeBoundary::from_css_shape] Path - fallback to rectangle".to_string(),
2284 ));
2285 }
2286 ShapeBoundary::Rectangle(reference_box)
2289 }
2290 };
2291
2292 if let Some(msgs) = debug_messages {
2293 msgs.push(LayoutDebugMessage::info(format!(
2294 "[ShapeBoundary::from_css_shape] Result: {:?}",
2295 result
2296 )));
2297 }
2298 result
2299 }
2300}
2301
2302#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2303pub struct InlineBreak {
2304 pub break_type: BreakType,
2305 pub clear: ClearType,
2306 pub content_index: usize,
2307}
2308
2309#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2310pub enum BreakType {
2311 Soft, Hard, Page, Column, }
2316
2317#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2318pub enum ClearType {
2319 None,
2320 Left,
2321 Right,
2322 Both,
2323}
2324
2325#[derive(Debug, Clone)]
2327pub struct ShapeConstraints {
2328 pub boundaries: Vec<ShapeBoundary>,
2329 pub exclusions: Vec<ShapeBoundary>,
2330 pub writing_mode: WritingMode,
2331 pub text_align: TextAlign,
2332 pub line_height: f32,
2333}
2334
2335#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
2336pub enum WritingMode {
2337 #[default]
2338 HorizontalTb, VerticalRl, VerticalLr, SidewaysRl, SidewaysLr, }
2344
2345impl WritingMode {
2346 pub fn is_advance_horizontal(&self) -> bool {
2348 matches!(
2349 self,
2350 WritingMode::HorizontalTb | WritingMode::SidewaysRl | WritingMode::SidewaysLr
2351 )
2352 }
2353}
2354
2355#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
2356pub enum JustifyContent {
2357 #[default]
2358 None,
2359 InterWord, InterCharacter, Distribute, Kashida, }
2364
2365#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
2367pub enum TextAlign {
2368 #[default]
2369 Left,
2370 Right,
2371 Center,
2372 Justify,
2373 Start,
2374 End, JustifyAll, }
2377
2378#[derive(Debug, Clone, Copy, PartialEq, Default, Eq, PartialOrd, Ord, Hash)]
2380pub enum TextOrientation {
2381 #[default]
2382 Mixed, Upright, Sideways, }
2386
2387#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2388pub struct TextDecoration {
2389 pub underline: bool,
2390 pub strikethrough: bool,
2391 pub overline: bool,
2392}
2393
2394impl Default for TextDecoration {
2395 fn default() -> Self {
2396 TextDecoration {
2397 underline: false,
2398 overline: false,
2399 strikethrough: false,
2400 }
2401 }
2402}
2403
2404impl TextDecoration {
2405 pub fn from_css(css: azul_css::props::style::text::StyleTextDecoration) -> Self {
2411 use azul_css::props::style::text::StyleTextDecoration;
2412 match css {
2413 StyleTextDecoration::None => Self::default(),
2414 StyleTextDecoration::Underline => Self {
2415 underline: true,
2416 strikethrough: false,
2417 overline: false,
2418 },
2419 StyleTextDecoration::Overline => Self {
2420 underline: false,
2421 strikethrough: false,
2422 overline: true,
2423 },
2424 StyleTextDecoration::LineThrough => Self {
2425 underline: false,
2426 strikethrough: true,
2427 overline: false,
2428 },
2429 }
2430 }
2431}
2432
2433#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2434pub enum TextTransform {
2435 #[default]
2436 None,
2437 Uppercase,
2438 Lowercase,
2439 Capitalize,
2440}
2441
2442pub type FourCc = [u8; 4];
2444
2445#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
2447pub enum Spacing {
2448 Px(i32), Em(f32),
2450}
2451
2452impl Eq for Spacing {}
2456
2457impl Hash for Spacing {
2458 fn hash<H: Hasher>(&self, state: &mut H) {
2459 discriminant(self).hash(state);
2461 match self {
2462 Spacing::Px(val) => val.hash(state),
2463 Spacing::Em(val) => val.to_bits().hash(state),
2466 }
2467 }
2468}
2469
2470impl Default for Spacing {
2471 fn default() -> Self {
2472 Spacing::Px(0)
2473 }
2474}
2475
2476impl Default for FontHash {
2477 fn default() -> Self {
2478 Self::invalid()
2479 }
2480}
2481
2482#[derive(Debug, Clone, PartialEq)]
2484pub struct StyleProperties {
2485 pub font_stack: FontStack,
2489 pub font_size_px: f32,
2490 pub color: ColorU,
2491 pub background_color: Option<ColorU>,
2502 pub background_content: Vec<StyleBackgroundContent>,
2505 pub border: Option<InlineBorderInfo>,
2507 pub letter_spacing: Spacing,
2508 pub word_spacing: Spacing,
2509
2510 pub line_height: f32,
2511 pub text_decoration: TextDecoration,
2512
2513 pub font_features: Vec<String>,
2515
2516 pub font_variations: Vec<(FourCc, f32)>,
2518 pub tab_size: f32,
2520 pub text_transform: TextTransform,
2522 pub writing_mode: WritingMode,
2524 pub text_orientation: TextOrientation,
2525 pub text_combine_upright: Option<TextCombineUpright>,
2527
2528 pub font_variant_caps: FontVariantCaps,
2530 pub font_variant_numeric: FontVariantNumeric,
2531 pub font_variant_ligatures: FontVariantLigatures,
2532 pub font_variant_east_asian: FontVariantEastAsian,
2533}
2534
2535impl Default for StyleProperties {
2536 fn default() -> Self {
2537 const FONT_SIZE: f32 = 16.0;
2538 const TAB_SIZE: f32 = 8.0;
2539 Self {
2540 font_stack: FontStack::default(),
2541 font_size_px: FONT_SIZE,
2542 color: ColorU::default(),
2543 background_color: None,
2544 background_content: Vec::new(),
2545 border: None,
2546 letter_spacing: Spacing::default(), word_spacing: Spacing::default(), line_height: FONT_SIZE * 1.2,
2549 text_decoration: TextDecoration::default(),
2550 font_features: Vec::new(),
2551 font_variations: Vec::new(),
2552 tab_size: TAB_SIZE, text_transform: TextTransform::default(),
2554 writing_mode: WritingMode::default(),
2555 text_orientation: TextOrientation::default(),
2556 text_combine_upright: None,
2557 font_variant_caps: FontVariantCaps::default(),
2558 font_variant_numeric: FontVariantNumeric::default(),
2559 font_variant_ligatures: FontVariantLigatures::default(),
2560 font_variant_east_asian: FontVariantEastAsian::default(),
2561 }
2562 }
2563}
2564
2565impl Hash for StyleProperties {
2566 fn hash<H: Hasher>(&self, state: &mut H) {
2567 self.font_stack.hash(state);
2568 self.color.hash(state);
2569 self.background_color.hash(state);
2570 self.text_decoration.hash(state);
2571 self.font_features.hash(state);
2572 self.writing_mode.hash(state);
2573 self.text_orientation.hash(state);
2574 self.text_combine_upright.hash(state);
2575 self.letter_spacing.hash(state);
2576 self.word_spacing.hash(state);
2577
2578 (self.font_size_px.round() as usize).hash(state);
2580 (self.line_height.round() as usize).hash(state);
2581 }
2582}
2583
2584impl StyleProperties {
2585 pub fn layout_hash(&self) -> u64 {
2602 use std::hash::Hasher;
2603 let mut hasher = std::collections::hash_map::DefaultHasher::new();
2604
2605 self.font_stack.hash(&mut hasher);
2607 (self.font_size_px.round() as usize).hash(&mut hasher);
2608 self.font_features.hash(&mut hasher);
2609 for (tag, value) in &self.font_variations {
2611 tag.hash(&mut hasher);
2612 (value.round() as i32).hash(&mut hasher);
2613 }
2614
2615 self.letter_spacing.hash(&mut hasher);
2617 self.word_spacing.hash(&mut hasher);
2618 (self.line_height.round() as usize).hash(&mut hasher);
2619 (self.tab_size.round() as usize).hash(&mut hasher);
2620
2621 self.writing_mode.hash(&mut hasher);
2623 self.text_orientation.hash(&mut hasher);
2624 self.text_combine_upright.hash(&mut hasher);
2625
2626 self.text_transform.hash(&mut hasher);
2628
2629 self.font_variant_caps.hash(&mut hasher);
2631 self.font_variant_numeric.hash(&mut hasher);
2632 self.font_variant_ligatures.hash(&mut hasher);
2633 self.font_variant_east_asian.hash(&mut hasher);
2634
2635 hasher.finish()
2636 }
2637
2638 pub fn layout_eq(&self, other: &Self) -> bool {
2642 self.layout_hash() == other.layout_hash()
2643 }
2644}
2645
2646#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
2647pub enum TextCombineUpright {
2648 None,
2649 All, Digits(u8), }
2652
2653#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2654pub enum GlyphSource {
2655 Char,
2657 Hyphen,
2659}
2660
2661#[derive(Debug, Clone, Copy, PartialEq)]
2662pub enum CharacterClass {
2663 Space, Punctuation, Letter, Ideograph, Symbol, Combining, }
2670
2671#[derive(Debug, Clone, Copy, PartialEq)]
2672pub enum GlyphOrientation {
2673 Horizontal, Vertical, Upright, Mixed, }
2678
2679#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2681pub enum BidiDirection {
2682 Ltr,
2683 Rtl,
2684}
2685
2686impl BidiDirection {
2687 pub fn is_rtl(&self) -> bool {
2688 matches!(self, BidiDirection::Rtl)
2689 }
2690}
2691
2692#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2693pub enum FontVariantCaps {
2694 #[default]
2695 Normal,
2696 SmallCaps,
2697 AllSmallCaps,
2698 PetiteCaps,
2699 AllPetiteCaps,
2700 Unicase,
2701 TitlingCaps,
2702}
2703
2704#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2705pub enum FontVariantNumeric {
2706 #[default]
2707 Normal,
2708 LiningNums,
2709 OldstyleNums,
2710 ProportionalNums,
2711 TabularNums,
2712 DiagonalFractions,
2713 StackedFractions,
2714 Ordinal,
2715 SlashedZero,
2716}
2717
2718#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2719pub enum FontVariantLigatures {
2720 #[default]
2721 Normal,
2722 None,
2723 Common,
2724 NoCommon,
2725 Discretionary,
2726 NoDiscretionary,
2727 Historical,
2728 NoHistorical,
2729 Contextual,
2730 NoContextual,
2731}
2732
2733#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2734pub enum FontVariantEastAsian {
2735 #[default]
2736 Normal,
2737 Jis78,
2738 Jis83,
2739 Jis90,
2740 Jis04,
2741 Simplified,
2742 Traditional,
2743 FullWidth,
2744 ProportionalWidth,
2745 Ruby,
2746}
2747
2748#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2749pub struct BidiLevel(u8);
2750
2751impl BidiLevel {
2752 pub fn new(level: u8) -> Self {
2753 Self(level)
2754 }
2755 pub fn is_rtl(&self) -> bool {
2756 self.0 % 2 == 1
2757 }
2758 pub fn level(&self) -> u8 {
2759 self.0
2760 }
2761}
2762
2763#[derive(Debug, Clone)]
2765pub struct StyleOverride {
2766 pub target: ContentIndex,
2768 pub style: PartialStyleProperties,
2771}
2772
2773#[derive(Debug, Clone, Default)]
2774pub struct PartialStyleProperties {
2775 pub font_stack: Option<FontStack>,
2776 pub font_size_px: Option<f32>,
2777 pub color: Option<ColorU>,
2778 pub letter_spacing: Option<Spacing>,
2779 pub word_spacing: Option<Spacing>,
2780 pub line_height: Option<f32>,
2781 pub text_decoration: Option<TextDecoration>,
2782 pub font_features: Option<Vec<String>>,
2783 pub font_variations: Option<Vec<(FourCc, f32)>>,
2784 pub tab_size: Option<f32>,
2785 pub text_transform: Option<TextTransform>,
2786 pub writing_mode: Option<WritingMode>,
2787 pub text_orientation: Option<TextOrientation>,
2788 pub text_combine_upright: Option<Option<TextCombineUpright>>,
2789 pub font_variant_caps: Option<FontVariantCaps>,
2790 pub font_variant_numeric: Option<FontVariantNumeric>,
2791 pub font_variant_ligatures: Option<FontVariantLigatures>,
2792 pub font_variant_east_asian: Option<FontVariantEastAsian>,
2793}
2794
2795impl Hash for PartialStyleProperties {
2796 fn hash<H: Hasher>(&self, state: &mut H) {
2797 self.font_stack.hash(state);
2798 self.font_size_px.map(|f| f.to_bits()).hash(state);
2799 self.color.hash(state);
2800 self.letter_spacing.hash(state);
2801 self.word_spacing.hash(state);
2802 self.line_height.map(|f| f.to_bits()).hash(state);
2803 self.text_decoration.hash(state);
2804 self.font_features.hash(state);
2805
2806 self.font_variations.as_ref().map(|v| {
2808 for (tag, val) in v {
2809 tag.hash(state);
2810 val.to_bits().hash(state);
2811 }
2812 });
2813
2814 self.tab_size.map(|f| f.to_bits()).hash(state);
2815 self.text_transform.hash(state);
2816 self.writing_mode.hash(state);
2817 self.text_orientation.hash(state);
2818 self.text_combine_upright.hash(state);
2819 self.font_variant_caps.hash(state);
2820 self.font_variant_numeric.hash(state);
2821 self.font_variant_ligatures.hash(state);
2822 self.font_variant_east_asian.hash(state);
2823 }
2824}
2825
2826impl PartialEq for PartialStyleProperties {
2827 fn eq(&self, other: &Self) -> bool {
2828 self.font_stack == other.font_stack &&
2829 self.font_size_px.map(|f| f.to_bits()) == other.font_size_px.map(|f| f.to_bits()) &&
2830 self.color == other.color &&
2831 self.letter_spacing == other.letter_spacing &&
2832 self.word_spacing == other.word_spacing &&
2833 self.line_height.map(|f| f.to_bits()) == other.line_height.map(|f| f.to_bits()) &&
2834 self.text_decoration == other.text_decoration &&
2835 self.font_features == other.font_features &&
2836 self.font_variations == other.font_variations && self.tab_size.map(|f| f.to_bits()) == other.tab_size.map(|f| f.to_bits()) &&
2838 self.text_transform == other.text_transform &&
2839 self.writing_mode == other.writing_mode &&
2840 self.text_orientation == other.text_orientation &&
2841 self.text_combine_upright == other.text_combine_upright &&
2842 self.font_variant_caps == other.font_variant_caps &&
2843 self.font_variant_numeric == other.font_variant_numeric &&
2844 self.font_variant_ligatures == other.font_variant_ligatures &&
2845 self.font_variant_east_asian == other.font_variant_east_asian
2846 }
2847}
2848
2849impl Eq for PartialStyleProperties {}
2850
2851impl StyleProperties {
2852 fn apply_override(&self, partial: &PartialStyleProperties) -> Self {
2853 let mut new_style = self.clone();
2854 if let Some(val) = &partial.font_stack {
2855 new_style.font_stack = val.clone();
2856 }
2857 if let Some(val) = partial.font_size_px {
2858 new_style.font_size_px = val;
2859 }
2860 if let Some(val) = &partial.color {
2861 new_style.color = val.clone();
2862 }
2863 if let Some(val) = partial.letter_spacing {
2864 new_style.letter_spacing = val;
2865 }
2866 if let Some(val) = partial.word_spacing {
2867 new_style.word_spacing = val;
2868 }
2869 if let Some(val) = partial.line_height {
2870 new_style.line_height = val;
2871 }
2872 if let Some(val) = &partial.text_decoration {
2873 new_style.text_decoration = val.clone();
2874 }
2875 if let Some(val) = &partial.font_features {
2876 new_style.font_features = val.clone();
2877 }
2878 if let Some(val) = &partial.font_variations {
2879 new_style.font_variations = val.clone();
2880 }
2881 if let Some(val) = partial.tab_size {
2882 new_style.tab_size = val;
2883 }
2884 if let Some(val) = partial.text_transform {
2885 new_style.text_transform = val;
2886 }
2887 if let Some(val) = partial.writing_mode {
2888 new_style.writing_mode = val;
2889 }
2890 if let Some(val) = partial.text_orientation {
2891 new_style.text_orientation = val;
2892 }
2893 if let Some(val) = &partial.text_combine_upright {
2894 new_style.text_combine_upright = val.clone();
2895 }
2896 if let Some(val) = partial.font_variant_caps {
2897 new_style.font_variant_caps = val;
2898 }
2899 if let Some(val) = partial.font_variant_numeric {
2900 new_style.font_variant_numeric = val;
2901 }
2902 if let Some(val) = partial.font_variant_ligatures {
2903 new_style.font_variant_ligatures = val;
2904 }
2905 if let Some(val) = partial.font_variant_east_asian {
2906 new_style.font_variant_east_asian = val;
2907 }
2908 new_style
2909 }
2910}
2911
2912#[derive(Debug, Clone, Copy, PartialEq)]
2914pub enum GlyphKind {
2915 Character,
2917 Hyphen,
2919 NotDef,
2921 Kashida {
2923 width: f32,
2925 },
2926}
2927
2928#[derive(Debug, Clone)]
2931pub enum LogicalItem {
2932 Text {
2933 source: ContentIndex,
2935 text: String,
2937 style: Arc<StyleProperties>,
2938 marker_position_outside: Option<bool>,
2942 source_node_id: Option<NodeId>,
2945 },
2946 CombinedText {
2948 source: ContentIndex,
2949 text: String,
2950 style: Arc<StyleProperties>,
2951 },
2952 Ruby {
2953 source: ContentIndex,
2954 base_text: String,
2957 ruby_text: String,
2958 style: Arc<StyleProperties>,
2959 },
2960 Object {
2961 source: ContentIndex,
2963 content: InlineContent,
2965 },
2966 Tab {
2967 source: ContentIndex,
2968 style: Arc<StyleProperties>,
2969 },
2970 Break {
2971 source: ContentIndex,
2972 break_info: InlineBreak,
2973 },
2974}
2975
2976impl Hash for LogicalItem {
2977 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2978 discriminant(self).hash(state);
2979 match self {
2980 LogicalItem::Text {
2981 source,
2982 text,
2983 style,
2984 marker_position_outside,
2985 source_node_id,
2986 } => {
2987 source.hash(state);
2988 text.hash(state);
2989 style.as_ref().hash(state); marker_position_outside.hash(state);
2991 source_node_id.hash(state);
2992 }
2993 LogicalItem::CombinedText {
2994 source,
2995 text,
2996 style,
2997 } => {
2998 source.hash(state);
2999 text.hash(state);
3000 style.as_ref().hash(state);
3001 }
3002 LogicalItem::Ruby {
3003 source,
3004 base_text,
3005 ruby_text,
3006 style,
3007 } => {
3008 source.hash(state);
3009 base_text.hash(state);
3010 ruby_text.hash(state);
3011 style.as_ref().hash(state);
3012 }
3013 LogicalItem::Object { source, content } => {
3014 source.hash(state);
3015 content.hash(state);
3016 }
3017 LogicalItem::Tab { source, style } => {
3018 source.hash(state);
3019 style.as_ref().hash(state);
3020 }
3021 LogicalItem::Break { source, break_info } => {
3022 source.hash(state);
3023 break_info.hash(state);
3024 }
3025 }
3026 }
3027}
3028
3029#[derive(Debug, Clone)]
3032pub struct VisualItem {
3033 pub logical_source: LogicalItem,
3036 pub bidi_level: BidiLevel,
3038 pub script: Script,
3040 pub text: String,
3042}
3043
3044#[derive(Debug, Clone)]
3047pub enum ShapedItem {
3048 Cluster(ShapedCluster),
3049 CombinedBlock {
3052 source: ContentIndex,
3053 glyphs: Vec<ShapedGlyph>,
3055 bounds: Rect,
3056 baseline_offset: f32,
3057 },
3058 Object {
3059 source: ContentIndex,
3060 bounds: Rect,
3061 baseline_offset: f32,
3062 content: InlineContent,
3064 },
3065 Tab {
3066 source: ContentIndex,
3067 bounds: Rect,
3068 },
3069 Break {
3070 source: ContentIndex,
3071 break_info: InlineBreak,
3072 },
3073}
3074
3075impl ShapedItem {
3076 pub fn as_cluster(&self) -> Option<&ShapedCluster> {
3077 match self {
3078 ShapedItem::Cluster(c) => Some(c),
3079 _ => None,
3080 }
3081 }
3082 pub fn bounds(&self) -> Rect {
3088 match self {
3089 ShapedItem::Cluster(cluster) => {
3090 let width = cluster.advance;
3092
3093 let (ascent, descent) = get_item_vertical_metrics(self);
3097 let height = ascent + descent;
3098
3099 Rect {
3100 x: 0.0,
3101 y: 0.0,
3102 width,
3103 height,
3104 }
3105 }
3106 ShapedItem::CombinedBlock { bounds, .. } => *bounds,
3109 ShapedItem::Object { bounds, .. } => *bounds,
3110 ShapedItem::Tab { bounds, .. } => *bounds,
3111
3112 ShapedItem::Break { .. } => Rect::default(), }
3115 }
3116}
3117
3118#[derive(Debug, Clone)]
3120pub struct ShapedCluster {
3121 pub text: String,
3124 pub source_cluster_id: GraphemeClusterId,
3126 pub source_content_index: ContentIndex,
3128 pub source_node_id: Option<NodeId>,
3131 pub glyphs: Vec<ShapedGlyph>,
3133 pub advance: f32,
3135 pub direction: BidiDirection,
3137 pub style: Arc<StyleProperties>,
3139 pub marker_position_outside: Option<bool>,
3143}
3144
3145#[derive(Debug, Clone)]
3147pub struct ShapedGlyph {
3148 pub kind: GlyphKind,
3150 pub glyph_id: u16,
3152 pub cluster_offset: u32,
3154 pub advance: f32,
3157 pub kerning: f32,
3160 pub offset: Point,
3162 pub vertical_advance: f32,
3164 pub vertical_offset: Point,
3166 pub script: Script,
3167 pub style: Arc<StyleProperties>,
3168 pub font_hash: u64,
3170 pub font_metrics: LayoutFontMetrics,
3172}
3173
3174impl ShapedGlyph {
3175 pub fn into_glyph_instance<T: ParsedFontTrait>(
3176 &self,
3177 writing_mode: WritingMode,
3178 loaded_fonts: &LoadedFonts<T>,
3179 ) -> GlyphInstance {
3180 let size = loaded_fonts
3181 .get_by_hash(self.font_hash)
3182 .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
3183 .unwrap_or_default();
3184
3185 let position = if writing_mode.is_advance_horizontal() {
3186 LogicalPosition {
3187 x: self.offset.x,
3188 y: self.offset.y,
3189 }
3190 } else {
3191 LogicalPosition {
3192 x: self.vertical_offset.x,
3193 y: self.vertical_offset.y,
3194 }
3195 };
3196
3197 GlyphInstance {
3198 index: self.glyph_id as u32,
3199 point: position,
3200 size,
3201 }
3202 }
3203
3204 pub fn into_glyph_instance_at<T: ParsedFontTrait>(
3207 &self,
3208 writing_mode: WritingMode,
3209 absolute_position: LogicalPosition,
3210 loaded_fonts: &LoadedFonts<T>,
3211 ) -> GlyphInstance {
3212 let size = loaded_fonts
3213 .get_by_hash(self.font_hash)
3214 .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
3215 .unwrap_or_default();
3216
3217 GlyphInstance {
3218 index: self.glyph_id as u32,
3219 point: absolute_position,
3220 size,
3221 }
3222 }
3223
3224 pub fn into_glyph_instance_at_simple(
3228 &self,
3229 _writing_mode: WritingMode,
3230 absolute_position: LogicalPosition,
3231 ) -> GlyphInstance {
3232 GlyphInstance {
3235 index: self.glyph_id as u32,
3236 point: absolute_position,
3237 size: LogicalSize::default(),
3238 }
3239 }
3240}
3241
3242#[derive(Debug, Clone)]
3245pub struct PositionedItem {
3246 pub item: ShapedItem,
3247 pub position: Point,
3248 pub line_index: usize,
3249}
3250
3251#[derive(Debug, Clone)]
3252pub struct UnifiedLayout {
3253 pub items: Vec<PositionedItem>,
3254 pub overflow: OverflowInfo,
3256}
3257
3258impl UnifiedLayout {
3259 pub fn bounds(&self) -> Rect {
3262 if self.items.is_empty() {
3263 return Rect::default();
3264 }
3265
3266 let mut min_x = f32::MAX;
3267 let mut min_y = f32::MAX;
3268 let mut max_x = f32::MIN;
3269 let mut max_y = f32::MIN;
3270
3271 for item in &self.items {
3272 let item_x = item.position.x;
3273 let item_y = item.position.y;
3274
3275 let item_bounds = item.item.bounds();
3277 let item_width = item_bounds.width;
3278 let item_height = item_bounds.height;
3279
3280 min_x = min_x.min(item_x);
3281 min_y = min_y.min(item_y);
3282 max_x = max_x.max(item_x + item_width);
3283 max_y = max_y.max(item_y + item_height);
3284 }
3285
3286 Rect {
3287 x: min_x,
3288 y: min_y,
3289 width: max_x - min_x,
3290 height: max_y - min_y,
3291 }
3292 }
3293
3294 pub fn is_empty(&self) -> bool {
3295 self.items.is_empty()
3296 }
3297 pub fn last_baseline(&self) -> Option<f32> {
3298 self.items
3299 .iter()
3300 .rev()
3301 .find_map(|item| get_baseline_for_item(&item.item))
3302 }
3303
3304 pub fn hittest_cursor(&self, point: LogicalPosition) -> Option<TextCursor> {
3310 if self.items.is_empty() {
3311 return None;
3312 }
3313
3314 let mut closest_item_idx = 0;
3316 let mut closest_distance = f32::MAX;
3317
3318 for (idx, item) in self.items.iter().enumerate() {
3319 if !matches!(item.item, ShapedItem::Cluster(_)) {
3321 continue;
3322 }
3323
3324 let item_bounds = item.item.bounds();
3325 let item_center_y = item.position.y + item_bounds.height / 2.0;
3326
3327 let vertical_distance = (point.y - item_center_y).abs();
3329
3330 let horizontal_distance = if point.x < item.position.x {
3332 item.position.x - point.x
3333 } else if point.x > item.position.x + item_bounds.width {
3334 point.x - (item.position.x + item_bounds.width)
3335 } else {
3336 0.0 };
3338
3339 let distance = vertical_distance * 2.0 + horizontal_distance;
3341
3342 if distance < closest_distance {
3343 closest_distance = distance;
3344 closest_item_idx = idx;
3345 }
3346 }
3347
3348 let closest_item = &self.items[closest_item_idx];
3350 let cluster = match &closest_item.item {
3351 ShapedItem::Cluster(c) => c,
3352 ShapedItem::Object { source, .. } | ShapedItem::CombinedBlock { source, .. } => {
3354 return Some(TextCursor {
3355 cluster_id: GraphemeClusterId {
3356 source_run: source.run_index,
3357 start_byte_in_run: source.item_index,
3358 },
3359 affinity: if point.x
3360 < closest_item.position.x + (closest_item.item.bounds().width / 2.0)
3361 {
3362 CursorAffinity::Leading
3363 } else {
3364 CursorAffinity::Trailing
3365 },
3366 });
3367 }
3368 _ => return None,
3369 };
3370
3371 let cluster_mid_x = closest_item.position.x + cluster.advance / 2.0;
3373 let affinity = if point.x < cluster_mid_x {
3374 CursorAffinity::Leading
3375 } else {
3376 CursorAffinity::Trailing
3377 };
3378
3379 Some(TextCursor {
3380 cluster_id: cluster.source_cluster_id,
3381 affinity,
3382 })
3383 }
3384
3385 pub fn get_selection_rects(&self, range: &SelectionRange) -> Vec<LogicalRect> {
3388 let mut cluster_map: HashMap<GraphemeClusterId, &PositionedItem> = HashMap::new();
3390 for item in &self.items {
3391 if let Some(cluster) = item.item.as_cluster() {
3392 cluster_map.insert(cluster.source_cluster_id, item);
3393 }
3394 }
3395
3396 let (start_cursor, end_cursor) = if range.start.cluster_id > range.end.cluster_id
3398 || (range.start.cluster_id == range.end.cluster_id
3399 && range.start.affinity > range.end.affinity)
3400 {
3401 (range.end, range.start)
3402 } else {
3403 (range.start, range.end)
3404 };
3405
3406 let Some(start_item) = cluster_map.get(&start_cursor.cluster_id) else {
3408 return Vec::new();
3409 };
3410 let Some(end_item) = cluster_map.get(&end_cursor.cluster_id) else {
3411 return Vec::new();
3412 };
3413
3414 let mut rects = Vec::new();
3415
3416 let get_cursor_x = |item: &PositionedItem, affinity: CursorAffinity| -> f32 {
3418 match affinity {
3419 CursorAffinity::Leading => item.position.x,
3420 CursorAffinity::Trailing => item.position.x + get_item_measure(&item.item, false),
3421 }
3422 };
3423
3424 let get_line_bounds = |line_index: usize| -> Option<LogicalRect> {
3426 let items_on_line = self.items.iter().filter(|i| i.line_index == line_index);
3427
3428 let mut min_x: Option<f32> = None;
3429 let mut max_x: Option<f32> = None;
3430 let mut min_y: Option<f32> = None;
3431 let mut max_y: Option<f32> = None;
3432
3433 for item in items_on_line {
3434 let item_bounds = item.item.bounds();
3436 if item_bounds.width <= 0.0 && item_bounds.height <= 0.0 {
3437 continue;
3438 }
3439
3440 let item_x_end = item.position.x + item_bounds.width;
3441 let item_y_end = item.position.y + item_bounds.height;
3442
3443 min_x = Some(min_x.map_or(item.position.x, |mx| mx.min(item.position.x)));
3444 max_x = Some(max_x.map_or(item_x_end, |mx| mx.max(item_x_end)));
3445 min_y = Some(min_y.map_or(item.position.y, |my| my.min(item.position.y)));
3446 max_y = Some(max_y.map_or(item_y_end, |my| my.max(item_y_end)));
3447 }
3448
3449 if let (Some(min_x), Some(max_x), Some(min_y), Some(max_y)) =
3450 (min_x, max_x, min_y, max_y)
3451 {
3452 Some(LogicalRect {
3453 origin: LogicalPosition { x: min_x, y: min_y },
3454 size: LogicalSize {
3455 width: max_x - min_x,
3456 height: max_y - min_y,
3457 },
3458 })
3459 } else {
3460 None
3461 }
3462 };
3463
3464 if start_item.line_index == end_item.line_index {
3466 if let Some(line_bounds) = get_line_bounds(start_item.line_index) {
3467 let start_x = get_cursor_x(start_item, start_cursor.affinity);
3468 let end_x = get_cursor_x(end_item, end_cursor.affinity);
3469
3470 rects.push(LogicalRect {
3472 origin: LogicalPosition {
3473 x: start_x.min(end_x),
3474 y: line_bounds.origin.y,
3475 },
3476 size: LogicalSize {
3477 width: (end_x - start_x).abs(),
3478 height: line_bounds.size.height,
3479 },
3480 });
3481 }
3482 }
3483 else {
3485 if let Some(start_line_bounds) = get_line_bounds(start_item.line_index) {
3487 let start_x = get_cursor_x(start_item, start_cursor.affinity);
3488 let line_end_x = start_line_bounds.origin.x + start_line_bounds.size.width;
3489 rects.push(LogicalRect {
3490 origin: LogicalPosition {
3491 x: start_x,
3492 y: start_line_bounds.origin.y,
3493 },
3494 size: LogicalSize {
3495 width: line_end_x - start_x,
3496 height: start_line_bounds.size.height,
3497 },
3498 });
3499 }
3500
3501 for line_idx in (start_item.line_index + 1)..end_item.line_index {
3503 if let Some(line_bounds) = get_line_bounds(line_idx) {
3504 rects.push(line_bounds);
3505 }
3506 }
3507
3508 if let Some(end_line_bounds) = get_line_bounds(end_item.line_index) {
3510 let line_start_x = end_line_bounds.origin.x;
3511 let end_x = get_cursor_x(end_item, end_cursor.affinity);
3512 rects.push(LogicalRect {
3513 origin: LogicalPosition {
3514 x: line_start_x,
3515 y: end_line_bounds.origin.y,
3516 },
3517 size: LogicalSize {
3518 width: end_x - line_start_x,
3519 height: end_line_bounds.size.height,
3520 },
3521 });
3522 }
3523 }
3524
3525 rects
3526 }
3527
3528 pub fn get_cursor_rect(&self, cursor: &TextCursor) -> Option<LogicalRect> {
3530 for item in &self.items {
3532 if let ShapedItem::Cluster(cluster) = &item.item {
3533 if cluster.source_cluster_id == cursor.cluster_id {
3534 let line_height = item.item.bounds().height;
3536 let cursor_x = match cursor.affinity {
3537 CursorAffinity::Leading => item.position.x,
3538 CursorAffinity::Trailing => item.position.x + cluster.advance,
3539 };
3540 return Some(LogicalRect {
3541 origin: LogicalPosition {
3542 x: cursor_x,
3543 y: item.position.y,
3544 },
3545 size: LogicalSize {
3546 width: 1.0,
3547 height: line_height,
3548 }, });
3550 }
3551 }
3552 }
3553 None
3554 }
3555
3556 pub fn get_first_cluster_cursor(&self) -> Option<TextCursor> {
3558 for item in &self.items {
3559 if let ShapedItem::Cluster(cluster) = &item.item {
3560 return Some(TextCursor {
3561 cluster_id: cluster.source_cluster_id,
3562 affinity: CursorAffinity::Leading,
3563 });
3564 }
3565 }
3566 None
3567 }
3568
3569 pub fn get_last_cluster_cursor(&self) -> Option<TextCursor> {
3571 for item in self.items.iter().rev() {
3572 if let ShapedItem::Cluster(cluster) = &item.item {
3573 return Some(TextCursor {
3574 cluster_id: cluster.source_cluster_id,
3575 affinity: CursorAffinity::Trailing,
3576 });
3577 }
3578 }
3579 None
3580 }
3581
3582 pub fn move_cursor_left(
3584 &self,
3585 cursor: TextCursor,
3586 debug: &mut Option<Vec<String>>,
3587 ) -> TextCursor {
3588 if let Some(d) = debug {
3589 d.push(format!(
3590 "[Cursor] move_cursor_left: starting at byte {}, affinity {:?}",
3591 cursor.cluster_id.start_byte_in_run, cursor.affinity
3592 ));
3593 }
3594
3595 let current_item_pos = self.items.iter().position(|i| {
3597 i.item
3598 .as_cluster()
3599 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3600 });
3601
3602 let Some(current_pos) = current_item_pos else {
3603 if let Some(d) = debug {
3604 d.push(format!(
3605 "[Cursor] move_cursor_left: cursor not found, staying at byte {}",
3606 cursor.cluster_id.start_byte_in_run
3607 ));
3608 }
3609 return cursor;
3610 };
3611
3612 if cursor.affinity == CursorAffinity::Trailing {
3614 if let Some(d) = debug {
3615 d.push(format!(
3616 "[Cursor] move_cursor_left: moving from trailing to leading edge of byte {}",
3617 cursor.cluster_id.start_byte_in_run
3618 ));
3619 }
3620 return TextCursor {
3621 cluster_id: cursor.cluster_id,
3622 affinity: CursorAffinity::Leading,
3623 };
3624 }
3625
3626 let current_line = self.items[current_pos].line_index;
3629
3630 if let Some(d) = debug {
3631 d.push(format!(
3632 "[Cursor] move_cursor_left: at leading edge, current line {}",
3633 current_line
3634 ));
3635 }
3636
3637 for i in (0..current_pos).rev() {
3639 if let Some(cluster) = self.items[i].item.as_cluster() {
3640 if self.items[i].line_index == current_line {
3641 if let Some(d) = debug {
3642 d.push(format!(
3643 "[Cursor] move_cursor_left: found previous cluster on same line, byte \
3644 {}",
3645 cluster.source_cluster_id.start_byte_in_run
3646 ));
3647 }
3648 return TextCursor {
3649 cluster_id: cluster.source_cluster_id,
3650 affinity: CursorAffinity::Trailing,
3651 };
3652 }
3653 }
3654 }
3655
3656 if current_line > 0 {
3658 let prev_line = current_line - 1;
3659 if let Some(d) = debug {
3660 d.push(format!(
3661 "[Cursor] move_cursor_left: trying previous line {}",
3662 prev_line
3663 ));
3664 }
3665 for i in (0..current_pos).rev() {
3666 if let Some(cluster) = self.items[i].item.as_cluster() {
3667 if self.items[i].line_index == prev_line {
3668 if let Some(d) = debug {
3669 d.push(format!(
3670 "[Cursor] move_cursor_left: found cluster on previous line, byte \
3671 {}",
3672 cluster.source_cluster_id.start_byte_in_run
3673 ));
3674 }
3675 return TextCursor {
3676 cluster_id: cluster.source_cluster_id,
3677 affinity: CursorAffinity::Trailing,
3678 };
3679 }
3680 }
3681 }
3682 }
3683
3684 if let Some(d) = debug {
3686 d.push(format!(
3687 "[Cursor] move_cursor_left: at start of text, staying at byte {}",
3688 cursor.cluster_id.start_byte_in_run
3689 ));
3690 }
3691 cursor
3692 }
3693
3694 pub fn move_cursor_right(
3696 &self,
3697 cursor: TextCursor,
3698 debug: &mut Option<Vec<String>>,
3699 ) -> TextCursor {
3700 if let Some(d) = debug {
3701 d.push(format!(
3702 "[Cursor] move_cursor_right: starting at byte {}, affinity {:?}",
3703 cursor.cluster_id.start_byte_in_run, cursor.affinity
3704 ));
3705 }
3706
3707 let current_item_pos = self.items.iter().position(|i| {
3709 i.item
3710 .as_cluster()
3711 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3712 });
3713
3714 let Some(current_pos) = current_item_pos else {
3715 if let Some(d) = debug {
3716 d.push(format!(
3717 "[Cursor] move_cursor_right: cursor not found, staying at byte {}",
3718 cursor.cluster_id.start_byte_in_run
3719 ));
3720 }
3721 return cursor;
3722 };
3723
3724 if cursor.affinity == CursorAffinity::Leading {
3726 if let Some(d) = debug {
3727 d.push(format!(
3728 "[Cursor] move_cursor_right: moving from leading to trailing edge of byte {}",
3729 cursor.cluster_id.start_byte_in_run
3730 ));
3731 }
3732 return TextCursor {
3733 cluster_id: cursor.cluster_id,
3734 affinity: CursorAffinity::Trailing,
3735 };
3736 }
3737
3738 let current_line = self.items[current_pos].line_index;
3740
3741 if let Some(d) = debug {
3742 d.push(format!(
3743 "[Cursor] move_cursor_right: at trailing edge, current line {}",
3744 current_line
3745 ));
3746 }
3747
3748 for i in (current_pos + 1)..self.items.len() {
3750 if let Some(cluster) = self.items[i].item.as_cluster() {
3751 if self.items[i].line_index == current_line {
3752 if let Some(d) = debug {
3753 d.push(format!(
3754 "[Cursor] move_cursor_right: found next cluster on same line, byte {}",
3755 cluster.source_cluster_id.start_byte_in_run
3756 ));
3757 }
3758 return TextCursor {
3759 cluster_id: cluster.source_cluster_id,
3760 affinity: CursorAffinity::Leading,
3761 };
3762 }
3763 }
3764 }
3765
3766 let next_line = current_line + 1;
3768 if let Some(d) = debug {
3769 d.push(format!(
3770 "[Cursor] move_cursor_right: trying next line {}",
3771 next_line
3772 ));
3773 }
3774 for i in (current_pos + 1)..self.items.len() {
3775 if let Some(cluster) = self.items[i].item.as_cluster() {
3776 if self.items[i].line_index == next_line {
3777 if let Some(d) = debug {
3778 d.push(format!(
3779 "[Cursor] move_cursor_right: found cluster on next line, byte {}",
3780 cluster.source_cluster_id.start_byte_in_run
3781 ));
3782 }
3783 return TextCursor {
3784 cluster_id: cluster.source_cluster_id,
3785 affinity: CursorAffinity::Leading,
3786 };
3787 }
3788 }
3789 }
3790
3791 if let Some(d) = debug {
3793 d.push(format!(
3794 "[Cursor] move_cursor_right: at end of text, staying at byte {}",
3795 cursor.cluster_id.start_byte_in_run
3796 ));
3797 }
3798 cursor
3799 }
3800
3801 pub fn move_cursor_up(
3803 &self,
3804 cursor: TextCursor,
3805 goal_x: &mut Option<f32>,
3806 debug: &mut Option<Vec<String>>,
3807 ) -> TextCursor {
3808 if let Some(d) = debug {
3809 d.push(format!(
3810 "[Cursor] move_cursor_up: from byte {} (affinity {:?})",
3811 cursor.cluster_id.start_byte_in_run, cursor.affinity
3812 ));
3813 }
3814
3815 let Some(current_item) = self.items.iter().find(|i| {
3816 i.item
3817 .as_cluster()
3818 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3819 }) else {
3820 if let Some(d) = debug {
3821 d.push(format!(
3822 "[Cursor] move_cursor_up: cursor not found in items, staying at byte {}",
3823 cursor.cluster_id.start_byte_in_run
3824 ));
3825 }
3826 return cursor;
3827 };
3828
3829 if let Some(d) = debug {
3830 d.push(format!(
3831 "[Cursor] move_cursor_up: current line {}, position ({}, {})",
3832 current_item.line_index, current_item.position.x, current_item.position.y
3833 ));
3834 }
3835
3836 let target_line_idx = current_item.line_index.saturating_sub(1);
3837 if current_item.line_index == target_line_idx {
3838 if let Some(d) = debug {
3839 d.push(format!(
3840 "[Cursor] move_cursor_up: already at top line {}, staying put",
3841 current_item.line_index
3842 ));
3843 }
3844 return cursor;
3845 }
3846
3847 let current_x = goal_x.unwrap_or_else(|| {
3848 let x = match cursor.affinity {
3849 CursorAffinity::Leading => current_item.position.x,
3850 CursorAffinity::Trailing => {
3851 current_item.position.x + get_item_measure(¤t_item.item, false)
3852 }
3853 };
3854 *goal_x = Some(x);
3855 x
3856 });
3857
3858 let target_y = self
3860 .items
3861 .iter()
3862 .find(|i| i.line_index == target_line_idx)
3863 .map(|i| i.position.y + (i.item.bounds().height / 2.0))
3864 .unwrap_or(current_item.position.y);
3865
3866 if let Some(d) = debug {
3867 d.push(format!(
3868 "[Cursor] move_cursor_up: target line {}, hittesting at ({}, {})",
3869 target_line_idx, current_x, target_y
3870 ));
3871 }
3872
3873 let result = self
3874 .hittest_cursor(LogicalPosition {
3875 x: current_x,
3876 y: target_y,
3877 })
3878 .unwrap_or(cursor);
3879
3880 if let Some(d) = debug {
3881 d.push(format!(
3882 "[Cursor] move_cursor_up: result byte {} (affinity {:?})",
3883 result.cluster_id.start_byte_in_run, result.affinity
3884 ));
3885 }
3886
3887 result
3888 }
3889
3890 pub fn move_cursor_down(
3892 &self,
3893 cursor: TextCursor,
3894 goal_x: &mut Option<f32>,
3895 debug: &mut Option<Vec<String>>,
3896 ) -> TextCursor {
3897 if let Some(d) = debug {
3898 d.push(format!(
3899 "[Cursor] move_cursor_down: from byte {} (affinity {:?})",
3900 cursor.cluster_id.start_byte_in_run, cursor.affinity
3901 ));
3902 }
3903
3904 let Some(current_item) = self.items.iter().find(|i| {
3905 i.item
3906 .as_cluster()
3907 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3908 }) else {
3909 if let Some(d) = debug {
3910 d.push(format!(
3911 "[Cursor] move_cursor_down: cursor not found in items, staying at byte {}",
3912 cursor.cluster_id.start_byte_in_run
3913 ));
3914 }
3915 return cursor;
3916 };
3917
3918 if let Some(d) = debug {
3919 d.push(format!(
3920 "[Cursor] move_cursor_down: current line {}, position ({}, {})",
3921 current_item.line_index, current_item.position.x, current_item.position.y
3922 ));
3923 }
3924
3925 let max_line = self.items.iter().map(|i| i.line_index).max().unwrap_or(0);
3926 let target_line_idx = (current_item.line_index + 1).min(max_line);
3927 if current_item.line_index == target_line_idx {
3928 if let Some(d) = debug {
3929 d.push(format!(
3930 "[Cursor] move_cursor_down: already at bottom line {}, staying put",
3931 current_item.line_index
3932 ));
3933 }
3934 return cursor;
3935 }
3936
3937 let current_x = goal_x.unwrap_or_else(|| {
3938 let x = match cursor.affinity {
3939 CursorAffinity::Leading => current_item.position.x,
3940 CursorAffinity::Trailing => {
3941 current_item.position.x + get_item_measure(¤t_item.item, false)
3942 }
3943 };
3944 *goal_x = Some(x);
3945 x
3946 });
3947
3948 let target_y = self
3949 .items
3950 .iter()
3951 .find(|i| i.line_index == target_line_idx)
3952 .map(|i| i.position.y + (i.item.bounds().height / 2.0))
3953 .unwrap_or(current_item.position.y);
3954
3955 if let Some(d) = debug {
3956 d.push(format!(
3957 "[Cursor] move_cursor_down: hit testing at ({}, {})",
3958 current_x, target_y
3959 ));
3960 }
3961
3962 let result = self
3963 .hittest_cursor(LogicalPosition {
3964 x: current_x,
3965 y: target_y,
3966 })
3967 .unwrap_or(cursor);
3968
3969 if let Some(d) = debug {
3970 d.push(format!(
3971 "[Cursor] move_cursor_down: result byte {}, affinity {:?}",
3972 result.cluster_id.start_byte_in_run, result.affinity
3973 ));
3974 }
3975
3976 result
3977 }
3978
3979 pub fn move_cursor_to_line_start(
3981 &self,
3982 cursor: TextCursor,
3983 debug: &mut Option<Vec<String>>,
3984 ) -> TextCursor {
3985 if let Some(d) = debug {
3986 d.push(format!(
3987 "[Cursor] move_cursor_to_line_start: starting at byte {}, affinity {:?}",
3988 cursor.cluster_id.start_byte_in_run, cursor.affinity
3989 ));
3990 }
3991
3992 let Some(current_item) = self.items.iter().find(|i| {
3993 i.item
3994 .as_cluster()
3995 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3996 }) else {
3997 if let Some(d) = debug {
3998 d.push(format!(
3999 "[Cursor] move_cursor_to_line_start: cursor not found, staying at byte {}",
4000 cursor.cluster_id.start_byte_in_run
4001 ));
4002 }
4003 return cursor;
4004 };
4005
4006 if let Some(d) = debug {
4007 d.push(format!(
4008 "[Cursor] move_cursor_to_line_start: current line {}, position ({}, {})",
4009 current_item.line_index, current_item.position.x, current_item.position.y
4010 ));
4011 }
4012
4013 let first_item_on_line = self
4014 .items
4015 .iter()
4016 .filter(|i| i.line_index == current_item.line_index)
4017 .min_by(|a, b| {
4018 a.position
4019 .x
4020 .partial_cmp(&b.position.x)
4021 .unwrap_or(Ordering::Equal)
4022 });
4023
4024 if let Some(item) = first_item_on_line {
4025 if let ShapedItem::Cluster(c) = &item.item {
4026 let result = TextCursor {
4027 cluster_id: c.source_cluster_id,
4028 affinity: CursorAffinity::Leading,
4029 };
4030 if let Some(d) = debug {
4031 d.push(format!(
4032 "[Cursor] move_cursor_to_line_start: result byte {}, affinity {:?}",
4033 result.cluster_id.start_byte_in_run, result.affinity
4034 ));
4035 }
4036 return result;
4037 }
4038 }
4039
4040 if let Some(d) = debug {
4041 d.push(format!(
4042 "[Cursor] move_cursor_to_line_start: no first item found, staying at byte {}",
4043 cursor.cluster_id.start_byte_in_run
4044 ));
4045 }
4046 cursor
4047 }
4048
4049 pub fn move_cursor_to_line_end(
4051 &self,
4052 cursor: TextCursor,
4053 debug: &mut Option<Vec<String>>,
4054 ) -> TextCursor {
4055 if let Some(d) = debug {
4056 d.push(format!(
4057 "[Cursor] move_cursor_to_line_end: starting at byte {}, affinity {:?}",
4058 cursor.cluster_id.start_byte_in_run, cursor.affinity
4059 ));
4060 }
4061
4062 let Some(current_item) = self.items.iter().find(|i| {
4063 i.item
4064 .as_cluster()
4065 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
4066 }) else {
4067 if let Some(d) = debug {
4068 d.push(format!(
4069 "[Cursor] move_cursor_to_line_end: cursor not found, staying at byte {}",
4070 cursor.cluster_id.start_byte_in_run
4071 ));
4072 }
4073 return cursor;
4074 };
4075
4076 if let Some(d) = debug {
4077 d.push(format!(
4078 "[Cursor] move_cursor_to_line_end: current line {}, position ({}, {})",
4079 current_item.line_index, current_item.position.x, current_item.position.y
4080 ));
4081 }
4082
4083 let last_item_on_line = self
4084 .items
4085 .iter()
4086 .filter(|i| i.line_index == current_item.line_index)
4087 .max_by(|a, b| {
4088 a.position
4089 .x
4090 .partial_cmp(&b.position.x)
4091 .unwrap_or(Ordering::Equal)
4092 });
4093
4094 if let Some(item) = last_item_on_line {
4095 if let ShapedItem::Cluster(c) = &item.item {
4096 let result = TextCursor {
4097 cluster_id: c.source_cluster_id,
4098 affinity: CursorAffinity::Trailing,
4099 };
4100 if let Some(d) = debug {
4101 d.push(format!(
4102 "[Cursor] move_cursor_to_line_end: result byte {}, affinity {:?}",
4103 result.cluster_id.start_byte_in_run, result.affinity
4104 ));
4105 }
4106 return result;
4107 }
4108 }
4109
4110 if let Some(d) = debug {
4111 d.push(format!(
4112 "[Cursor] move_cursor_to_line_end: no last item found, staying at byte {}",
4113 cursor.cluster_id.start_byte_in_run
4114 ));
4115 }
4116 cursor
4117 }
4118}
4119
4120fn get_baseline_for_item(item: &ShapedItem) -> Option<f32> {
4121 match item {
4122 ShapedItem::CombinedBlock {
4123 baseline_offset, ..
4124 } => Some(*baseline_offset),
4125 ShapedItem::Object {
4126 baseline_offset, ..
4127 } => Some(*baseline_offset),
4128 ShapedItem::Cluster(ref cluster) => {
4130 if let Some(last_glyph) = cluster.glyphs.last() {
4131 Some(
4132 last_glyph
4133 .font_metrics
4134 .baseline_scaled(last_glyph.style.font_size_px),
4135 )
4136 } else {
4137 None
4138 }
4139 }
4140 ShapedItem::Break { source, break_info } => {
4141 None
4143 }
4144 ShapedItem::Tab { source, bounds } => {
4145 None
4147 }
4148 }
4149}
4150
4151#[derive(Debug, Clone, Default)]
4153pub struct OverflowInfo {
4154 pub overflow_items: Vec<ShapedItem>,
4156 pub unclipped_bounds: Rect,
4159}
4160
4161impl OverflowInfo {
4162 pub fn has_overflow(&self) -> bool {
4163 !self.overflow_items.is_empty()
4164 }
4165}
4166
4167#[derive(Debug, Clone)]
4169pub struct UnifiedLine {
4170 pub items: Vec<ShapedItem>,
4171 pub cross_axis_position: f32,
4173 pub constraints: LineConstraints,
4175 pub is_last: bool,
4176}
4177
4178pub type CacheId = u64;
4181
4182#[derive(Debug, Clone)]
4184pub struct LayoutFragment {
4185 pub id: String,
4187 pub constraints: UnifiedConstraints,
4189}
4190
4191#[derive(Debug, Clone)]
4193pub struct FlowLayout {
4194 pub fragment_layouts: HashMap<String, Arc<UnifiedLayout>>,
4196 pub remaining_items: Vec<ShapedItem>,
4199}
4200
4201pub struct LayoutCache {
4202 logical_items: HashMap<CacheId, Arc<Vec<LogicalItem>>>,
4204 visual_items: HashMap<CacheId, Arc<Vec<VisualItem>>>,
4206 shaped_items: HashMap<CacheId, Arc<Vec<ShapedItem>>>,
4208 layouts: HashMap<CacheId, Arc<UnifiedLayout>>,
4210}
4211
4212impl LayoutCache {
4213 pub fn new() -> Self {
4214 Self {
4215 logical_items: HashMap::new(),
4216 visual_items: HashMap::new(),
4217 shaped_items: HashMap::new(),
4218 layouts: HashMap::new(),
4219 }
4220 }
4221
4222 pub fn get_layout(&self, cache_id: &CacheId) -> Option<&Arc<UnifiedLayout>> {
4224 self.layouts.get(cache_id)
4225 }
4226
4227 pub fn get_all_layout_ids(&self) -> Vec<CacheId> {
4229 self.layouts.keys().copied().collect()
4230 }
4231
4232 pub fn use_old_layout(
4247 old_constraints: &UnifiedConstraints,
4248 new_constraints: &UnifiedConstraints,
4249 old_content: &[InlineContent],
4250 new_content: &[InlineContent],
4251 ) -> bool {
4252 if old_constraints != new_constraints {
4254 return false;
4255 }
4256
4257 if old_content.len() != new_content.len() {
4259 return false;
4260 }
4261
4262 for (old, new) in old_content.iter().zip(new_content.iter()) {
4264 if !Self::inline_content_layout_eq(old, new) {
4265 return false;
4266 }
4267 }
4268
4269 true
4270 }
4271
4272 fn inline_content_layout_eq(old: &InlineContent, new: &InlineContent) -> bool {
4276 use InlineContent::*;
4277 match (old, new) {
4278 (Text(old_run), Text(new_run)) => {
4279 old_run.text == new_run.text
4281 && old_run.style.layout_eq(&new_run.style)
4282 }
4283 (Image(old_img), Image(new_img)) => {
4284 old_img.intrinsic_size == new_img.intrinsic_size
4286 && old_img.display_size == new_img.display_size
4287 && old_img.baseline_offset == new_img.baseline_offset
4288 && old_img.alignment == new_img.alignment
4289 }
4290 (Space(old_sp), Space(new_sp)) => old_sp == new_sp,
4291 (LineBreak(old_br), LineBreak(new_br)) => old_br == new_br,
4292 (Tab { style: old_style }, Tab { style: new_style }) => old_style.layout_eq(new_style),
4293 (Marker { run: old_run, position_outside: old_pos },
4294 Marker { run: new_run, position_outside: new_pos }) => {
4295 old_pos == new_pos
4296 && old_run.text == new_run.text
4297 && old_run.style.layout_eq(&new_run.style)
4298 }
4299 (Shape(old_shape), Shape(new_shape)) => {
4300 old_shape.shape_def == new_shape.shape_def
4302 && old_shape.baseline_offset == new_shape.baseline_offset
4303 }
4304 (Ruby { base: old_base, text: old_text, style: old_style },
4305 Ruby { base: new_base, text: new_text, style: new_style }) => {
4306 old_style.layout_eq(new_style)
4307 && old_base.len() == new_base.len()
4308 && old_text.len() == new_text.len()
4309 && old_base.iter().zip(new_base.iter())
4310 .all(|(o, n)| Self::inline_content_layout_eq(o, n))
4311 && old_text.iter().zip(new_text.iter())
4312 .all(|(o, n)| Self::inline_content_layout_eq(o, n))
4313 }
4314 _ => false,
4316 }
4317 }
4318}
4319
4320impl Default for LayoutCache {
4321 fn default() -> Self {
4322 Self::new()
4323 }
4324}
4325
4326#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4328pub struct LogicalItemsKey<'a> {
4329 pub inline_content_hash: u64, pub default_font_size: u32, pub _marker: std::marker::PhantomData<&'a ()>,
4333}
4334
4335#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4337pub struct VisualItemsKey {
4338 pub logical_items_id: CacheId,
4339 pub base_direction: BidiDirection,
4340}
4341
4342#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4344pub struct ShapedItemsKey {
4345 pub visual_items_id: CacheId,
4346 pub style_hash: u64, }
4348
4349impl ShapedItemsKey {
4350 pub fn new(visual_items_id: CacheId, visual_items: &[VisualItem]) -> Self {
4351 let style_hash = {
4352 let mut hasher = DefaultHasher::new();
4353 for item in visual_items.iter() {
4354 match &item.logical_source {
4356 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
4357 style.as_ref().hash(&mut hasher);
4358 }
4359 _ => {}
4360 }
4361 }
4362 hasher.finish()
4363 };
4364
4365 Self {
4366 visual_items_id,
4367 style_hash,
4368 }
4369 }
4370}
4371
4372#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4374pub struct LayoutKey {
4375 pub shaped_items_id: CacheId,
4376 pub constraints: UnifiedConstraints,
4377}
4378
4379fn calculate_id<T: Hash>(item: &T) -> CacheId {
4381 let mut hasher = DefaultHasher::new();
4382 item.hash(&mut hasher);
4383 hasher.finish()
4384}
4385
4386impl LayoutCache {
4389 pub fn layout_flow<T: ParsedFontTrait>(
4447 &mut self,
4448 content: &[InlineContent],
4449 style_overrides: &[StyleOverride],
4450 flow_chain: &[LayoutFragment],
4451 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
4452 fc_cache: &FcFontCache,
4453 loaded_fonts: &LoadedFonts<T>,
4454 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4455 ) -> Result<FlowLayout, LayoutError> {
4456 let logical_items_id = calculate_id(&content);
4462 let logical_items = self
4463 .logical_items
4464 .entry(logical_items_id)
4465 .or_insert_with(|| {
4466 Arc::new(create_logical_items(
4467 content,
4468 style_overrides,
4469 debug_messages,
4470 ))
4471 })
4472 .clone();
4473
4474 let default_constraints = UnifiedConstraints::default();
4477 let first_constraints = flow_chain
4478 .first()
4479 .map(|f| &f.constraints)
4480 .unwrap_or(&default_constraints);
4481
4482 let base_direction = first_constraints.direction.unwrap_or(BidiDirection::Ltr);
4489 let visual_key = VisualItemsKey {
4490 logical_items_id,
4491 base_direction,
4492 };
4493 let visual_items_id = calculate_id(&visual_key);
4494 let visual_items = self
4495 .visual_items
4496 .entry(visual_items_id)
4497 .or_insert_with(|| {
4498 Arc::new(
4499 reorder_logical_items(&logical_items, base_direction, debug_messages).unwrap(),
4500 )
4501 })
4502 .clone();
4503
4504 let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
4506 let shaped_items_id = calculate_id(&shaped_key);
4507 let shaped_items = match self.shaped_items.get(&shaped_items_id) {
4508 Some(cached) => {
4509 cached.clone()
4510 }
4511 None => {
4512 let items = Arc::new(shape_visual_items(
4513 &visual_items,
4514 font_chain_cache,
4515 fc_cache,
4516 loaded_fonts,
4517 debug_messages,
4518 )?);
4519 self.shaped_items.insert(shaped_items_id, items.clone());
4520 items
4521 }
4522 };
4523
4524 let oriented_items = apply_text_orientation(shaped_items, first_constraints)?;
4531
4532 let mut fragment_layouts = HashMap::new();
4534 let mut cursor = BreakCursor::new(&oriented_items);
4536
4537 for fragment in flow_chain {
4538 let fragment_layout = perform_fragment_layout(
4540 &mut cursor,
4541 &logical_items,
4542 &fragment.constraints,
4543 debug_messages,
4544 loaded_fonts,
4545 )?;
4546
4547 fragment_layouts.insert(fragment.id.clone(), Arc::new(fragment_layout));
4548 if cursor.is_done() {
4549 break; }
4551 }
4552
4553 Ok(FlowLayout {
4554 fragment_layouts,
4555 remaining_items: cursor.drain_remaining(),
4556 })
4557 }
4558}
4559
4560pub fn create_logical_items(
4562 content: &[InlineContent],
4563 style_overrides: &[StyleOverride],
4564 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4565) -> Vec<LogicalItem> {
4566 if let Some(msgs) = debug_messages {
4567 msgs.push(LayoutDebugMessage::info(
4568 "\n--- Entering create_logical_items (Refactored) ---".to_string(),
4569 ));
4570 msgs.push(LayoutDebugMessage::info(format!(
4571 "Input content length: {}",
4572 content.len()
4573 )));
4574 msgs.push(LayoutDebugMessage::info(format!(
4575 "Input overrides length: {}",
4576 style_overrides.len()
4577 )));
4578 }
4579
4580 let mut items = Vec::new();
4581 let mut style_cache: HashMap<u64, Arc<StyleProperties>> = HashMap::new();
4582
4583 let mut run_overrides: HashMap<u32, HashMap<u32, &PartialStyleProperties>> = HashMap::new();
4585 for override_item in style_overrides {
4586 run_overrides
4587 .entry(override_item.target.run_index)
4588 .or_default()
4589 .insert(override_item.target.item_index, &override_item.style);
4590 }
4591
4592 for (run_idx, inline_item) in content.iter().enumerate() {
4593 if let Some(msgs) = debug_messages {
4594 msgs.push(LayoutDebugMessage::info(format!(
4595 "Processing content run #{}",
4596 run_idx
4597 )));
4598 }
4599
4600 let marker_position_outside = match inline_item {
4602 InlineContent::Marker {
4603 position_outside, ..
4604 } => Some(*position_outside),
4605 _ => None,
4606 };
4607
4608 match inline_item {
4609 InlineContent::Text(run) | InlineContent::Marker { run, .. } => {
4610 let text = &run.text;
4611 if text.is_empty() {
4612 if let Some(msgs) = debug_messages {
4613 msgs.push(LayoutDebugMessage::info(
4614 " Run is empty, skipping.".to_string(),
4615 ));
4616 }
4617 continue;
4618 }
4619 if let Some(msgs) = debug_messages {
4620 msgs.push(LayoutDebugMessage::info(format!(" Run text: '{}'", text)));
4621 }
4622
4623 let current_run_overrides = run_overrides.get(&(run_idx as u32));
4624 let mut boundaries = BTreeSet::new();
4625 boundaries.insert(0);
4626 boundaries.insert(text.len());
4627
4628 let mut scan_cursor = 0;
4630 while scan_cursor < text.len() {
4631 let style_at_cursor = if let Some(partial) =
4632 current_run_overrides.and_then(|o| o.get(&(scan_cursor as u32)))
4633 {
4634 run.style.apply_override(partial)
4636 } else {
4637 (*run.style).clone()
4638 };
4639
4640 let current_char = text[scan_cursor..].chars().next().unwrap();
4641
4642 if let Some(TextCombineUpright::Digits(max_digits)) =
4644 style_at_cursor.text_combine_upright
4645 {
4646 if max_digits > 0 && current_char.is_ascii_digit() {
4647 let digit_chunk: String = text[scan_cursor..]
4648 .chars()
4649 .take(max_digits as usize)
4650 .take_while(|c| c.is_ascii_digit())
4651 .collect();
4652
4653 let end_of_chunk = scan_cursor + digit_chunk.len();
4654 boundaries.insert(scan_cursor);
4655 boundaries.insert(end_of_chunk);
4656 scan_cursor = end_of_chunk; continue;
4658 }
4659 }
4660
4661 if current_run_overrides
4664 .and_then(|o| o.get(&(scan_cursor as u32)))
4665 .is_some()
4666 {
4667 let grapheme_len = text[scan_cursor..]
4668 .graphemes(true)
4669 .next()
4670 .unwrap_or("")
4671 .len();
4672 boundaries.insert(scan_cursor);
4673 boundaries.insert(scan_cursor + grapheme_len);
4674 scan_cursor += grapheme_len;
4675 continue;
4676 }
4677
4678 scan_cursor += current_char.len_utf8();
4681 }
4682
4683 if let Some(msgs) = debug_messages {
4684 msgs.push(LayoutDebugMessage::info(format!(
4685 " Boundaries: {:?}",
4686 boundaries
4687 )));
4688 }
4689
4690 for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
4692 let (start, end) = (*start, *end);
4693 if start >= end {
4694 continue;
4695 }
4696
4697 let text_slice = &text[start..end];
4698 if let Some(msgs) = debug_messages {
4699 msgs.push(LayoutDebugMessage::info(format!(
4700 " Processing chunk from {} to {}: '{}'",
4701 start, end, text_slice
4702 )));
4703 }
4704
4705 let style_to_use = if let Some(partial_style) =
4706 current_run_overrides.and_then(|o| o.get(&(start as u32)))
4707 {
4708 if let Some(msgs) = debug_messages {
4709 msgs.push(LayoutDebugMessage::info(format!(
4710 " -> Applying override at byte {}",
4711 start
4712 )));
4713 }
4714 let mut hasher = DefaultHasher::new();
4715 Arc::as_ptr(&run.style).hash(&mut hasher);
4716 partial_style.hash(&mut hasher);
4717 style_cache
4718 .entry(hasher.finish())
4719 .or_insert_with(|| Arc::new(run.style.apply_override(partial_style)))
4720 .clone()
4721 } else {
4722 run.style.clone()
4723 };
4724
4725 let is_combinable_chunk = if let Some(TextCombineUpright::Digits(max_digits)) =
4726 &style_to_use.text_combine_upright
4727 {
4728 *max_digits > 0
4729 && !text_slice.is_empty()
4730 && text_slice.chars().all(|c| c.is_ascii_digit())
4731 && text_slice.chars().count() <= *max_digits as usize
4732 } else {
4733 false
4734 };
4735
4736 if is_combinable_chunk {
4737 items.push(LogicalItem::CombinedText {
4738 source: ContentIndex {
4739 run_index: run_idx as u32,
4740 item_index: start as u32,
4741 },
4742 text: text_slice.to_string(),
4743 style: style_to_use,
4744 });
4745 } else {
4746 items.push(LogicalItem::Text {
4747 source: ContentIndex {
4748 run_index: run_idx as u32,
4749 item_index: start as u32,
4750 },
4751 text: text_slice.to_string(),
4752 style: style_to_use,
4753 marker_position_outside,
4754 source_node_id: run.source_node_id,
4755 });
4756 }
4757 }
4758 }
4759 InlineContent::LineBreak(break_info) => {
4761 if let Some(msgs) = debug_messages {
4762 msgs.push(LayoutDebugMessage::info(format!(
4763 " LineBreak: {:?}",
4764 break_info
4765 )));
4766 }
4767 items.push(LogicalItem::Break {
4768 source: ContentIndex {
4769 run_index: run_idx as u32,
4770 item_index: 0,
4771 },
4772 break_info: break_info.clone(),
4773 });
4774 }
4775 InlineContent::Tab { style } => {
4777 if let Some(msgs) = debug_messages {
4778 msgs.push(LayoutDebugMessage::info(" Tab character".to_string()));
4779 }
4780 items.push(LogicalItem::Tab {
4781 source: ContentIndex {
4782 run_index: run_idx as u32,
4783 item_index: 0,
4784 },
4785 style: style.clone(),
4786 });
4787 }
4788 _ => {
4790 if let Some(msgs) = debug_messages {
4791 msgs.push(LayoutDebugMessage::info(
4792 " Run is not text, creating generic LogicalItem.".to_string(),
4793 ));
4794 }
4795 items.push(LogicalItem::Object {
4796 source: ContentIndex {
4797 run_index: run_idx as u32,
4798 item_index: 0,
4799 },
4800 content: inline_item.clone(),
4801 });
4802 }
4803 }
4804 }
4805 if let Some(msgs) = debug_messages {
4806 msgs.push(LayoutDebugMessage::info(format!(
4807 "--- Exiting create_logical_items, created {} items ---",
4808 items.len()
4809 )));
4810 }
4811 items
4812}
4813
4814pub fn get_base_direction_from_logical(logical_items: &[LogicalItem]) -> BidiDirection {
4817 let first_strong = logical_items.iter().find_map(|item| {
4818 if let LogicalItem::Text { text, .. } = item {
4819 Some(unicode_bidi::get_base_direction(text.as_str()))
4820 } else {
4821 None
4822 }
4823 });
4824
4825 match first_strong {
4826 Some(unicode_bidi::Direction::Rtl) => BidiDirection::Rtl,
4827 _ => BidiDirection::Ltr,
4828 }
4829}
4830
4831pub fn reorder_logical_items(
4832 logical_items: &[LogicalItem],
4833 base_direction: BidiDirection,
4834 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4835) -> Result<Vec<VisualItem>, LayoutError> {
4836 if let Some(msgs) = debug_messages {
4837 msgs.push(LayoutDebugMessage::info(
4838 "\n--- Entering reorder_logical_items ---".to_string(),
4839 ));
4840 msgs.push(LayoutDebugMessage::info(format!(
4841 "Input logical items count: {}",
4842 logical_items.len()
4843 )));
4844 msgs.push(LayoutDebugMessage::info(format!(
4845 "Base direction: {:?}",
4846 base_direction
4847 )));
4848 }
4849
4850 let mut bidi_str = String::new();
4851 let mut item_map = Vec::new();
4852 for (idx, item) in logical_items.iter().enumerate() {
4853 let text = match item {
4854 LogicalItem::Text { text, .. } => text.as_str(),
4855 LogicalItem::CombinedText { text, .. } => text.as_str(),
4856 _ => "\u{FFFC}",
4857 };
4858 let start_byte = bidi_str.len();
4859 bidi_str.push_str(text);
4860 for _ in start_byte..bidi_str.len() {
4861 item_map.push(idx);
4862 }
4863 }
4864
4865 if bidi_str.is_empty() {
4866 if let Some(msgs) = debug_messages {
4867 msgs.push(LayoutDebugMessage::info(
4868 "Bidi string is empty, returning.".to_string(),
4869 ));
4870 }
4871 return Ok(Vec::new());
4872 }
4873 if let Some(msgs) = debug_messages {
4874 msgs.push(LayoutDebugMessage::info(format!(
4875 "Constructed bidi string: '{}'",
4876 bidi_str
4877 )));
4878 }
4879
4880 let bidi_level = if base_direction == BidiDirection::Rtl {
4881 Some(Level::rtl())
4882 } else {
4883 Some(Level::ltr())
4884 };
4885 let bidi_info = BidiInfo::new(&bidi_str, bidi_level);
4886 let para = &bidi_info.paragraphs[0];
4887 let (levels, visual_runs) = bidi_info.visual_runs(para, para.range.clone());
4888
4889 if let Some(msgs) = debug_messages {
4890 msgs.push(LayoutDebugMessage::info(
4891 "Bidi visual runs generated:".to_string(),
4892 ));
4893 for (i, run_range) in visual_runs.iter().enumerate() {
4894 let level = levels[run_range.start].number();
4895 let slice = &bidi_str[run_range.start..run_range.end];
4896 msgs.push(LayoutDebugMessage::info(format!(
4897 " Run {}: range={:?}, level={}, text='{}'",
4898 i, run_range, level, slice
4899 )));
4900 }
4901 }
4902
4903 let mut visual_items = Vec::new();
4904 for run_range in visual_runs {
4905 let bidi_level = BidiLevel::new(levels[run_range.start].number());
4906 let mut sub_run_start = run_range.start;
4907
4908 for i in (run_range.start + 1)..run_range.end {
4909 if item_map[i] != item_map[sub_run_start] {
4910 let logical_idx = item_map[sub_run_start];
4911 let logical_item = &logical_items[logical_idx];
4912 let text_slice = &bidi_str[sub_run_start..i];
4913 visual_items.push(VisualItem {
4914 logical_source: logical_item.clone(),
4915 bidi_level,
4916 script: crate::text3::script::detect_script(text_slice)
4917 .unwrap_or(Script::Latin),
4918 text: text_slice.to_string(),
4919 });
4920 sub_run_start = i;
4921 }
4922 }
4923
4924 let logical_idx = item_map[sub_run_start];
4925 let logical_item = &logical_items[logical_idx];
4926 let text_slice = &bidi_str[sub_run_start..run_range.end];
4927 visual_items.push(VisualItem {
4928 logical_source: logical_item.clone(),
4929 bidi_level,
4930 script: crate::text3::script::detect_script(text_slice).unwrap_or(Script::Latin),
4931 text: text_slice.to_string(),
4932 });
4933 }
4934
4935 if let Some(msgs) = debug_messages {
4936 msgs.push(LayoutDebugMessage::info(
4937 "Final visual items produced:".to_string(),
4938 ));
4939 for (i, item) in visual_items.iter().enumerate() {
4940 msgs.push(LayoutDebugMessage::info(format!(
4941 " Item {}: level={}, text='{}'",
4942 i,
4943 item.bidi_level.level(),
4944 item.text
4945 )));
4946 }
4947 msgs.push(LayoutDebugMessage::info(
4948 "--- Exiting reorder_logical_items ---".to_string(),
4949 ));
4950 }
4951 Ok(visual_items)
4952}
4953
4954pub fn shape_visual_items<T: ParsedFontTrait>(
4971 visual_items: &[VisualItem],
4972 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
4973 fc_cache: &FcFontCache,
4974 loaded_fonts: &LoadedFonts<T>,
4975 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4976) -> Result<Vec<ShapedItem>, LayoutError> {
4977 let mut shaped = Vec::new();
4978 let mut idx = 0;
4979 let mut _coalesced_runs = 0usize;
4980 let mut _total_runs = 0usize;
4981 let mut _shape_calls = 0usize;
4982
4983 while idx < visual_items.len() {
4986 let item = &visual_items[idx];
4987 match &item.logical_source {
4988 LogicalItem::Text {
4989 style,
4990 source,
4991 marker_position_outside,
4992 source_node_id,
4993 ..
4994 } => {
4995 let layout_hash = style.layout_hash();
4996 let bidi_level = item.bidi_level;
4997 let script = item.script;
4998
4999 let mut coalesce_end = idx + 1;
5002 while coalesce_end < visual_items.len() {
5003 let next = &visual_items[coalesce_end];
5004 if let LogicalItem::Text { style: next_style, .. } = &next.logical_source {
5005 if next_style.layout_hash() == layout_hash
5006 && next.bidi_level == bidi_level
5007 && next.script == script
5008 {
5009 coalesce_end += 1;
5010 } else {
5011 break;
5012 }
5013 } else {
5014 break;
5015 }
5016 }
5017
5018 let coalesce_count = coalesce_end - idx;
5019
5020 if coalesce_count > 1 {
5021 _coalesced_runs += coalesce_count;
5022 _shape_calls += 1;
5023 let total_text_len: usize = visual_items[idx..coalesce_end]
5029 .iter()
5030 .map(|v| v.text.len())
5031 .sum();
5032 let mut merged_text = String::with_capacity(total_text_len);
5033 let mut byte_ranges: Vec<(
5035 usize, usize,
5036 Arc<StyleProperties>,
5037 ContentIndex,
5038 Option<NodeId>,
5039 Option<bool>,
5040 )> = Vec::with_capacity(coalesce_count);
5041
5042 for j in idx..coalesce_end {
5043 let start = merged_text.len();
5044 merged_text.push_str(&visual_items[j].text);
5045 let end = merged_text.len();
5046 if let LogicalItem::Text {
5047 style: s, source: src, source_node_id: nid,
5048 marker_position_outside: mpo, ..
5049 } = &visual_items[j].logical_source {
5050 byte_ranges.push((start, end, s.clone(), *src, *nid, *mpo));
5051 }
5052 }
5053
5054 if let Some(msgs) = debug_messages {
5055 msgs.push(LayoutDebugMessage::info(format!(
5056 "[TextLayout] Coalescing {} text runs ({} bytes) into single shaping call",
5057 coalesce_count, merged_text.len()
5058 )));
5059 }
5060
5061 let direction = if bidi_level.is_rtl() {
5062 BidiDirection::Rtl
5063 } else {
5064 BidiDirection::Ltr
5065 };
5066 let language = script_to_language(script, &merged_text);
5067
5068 let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
5071 FontStack::Ref(font_ref) => {
5072 shape_text_correctly(
5073 &merged_text, script, language, direction,
5074 font_ref, style, *source, *source_node_id,
5075 )
5076 }
5077 FontStack::Stack(selectors) => {
5078 let cache_key = FontChainKey::from_selectors(selectors);
5079 let font_chain = match font_chain_cache.get(&cache_key) {
5080 Some(chain) => chain,
5081 None => { idx = coalesce_end; continue; }
5082 };
5083 let first_char = merged_text.chars().next().unwrap_or('A');
5084 let font_id = match font_chain.resolve_char(fc_cache, first_char) {
5085 Some((id, _)) => id,
5086 None => { idx = coalesce_end; continue; }
5087 };
5088 match loaded_fonts.get(&font_id) {
5089 Some(font) => shape_text_correctly(
5090 &merged_text, script, language, direction,
5091 font, style, *source, *source_node_id,
5092 ),
5093 None => { idx = coalesce_end; continue; }
5094 }
5095 }
5096 };
5097
5098 let shaped_clusters = shaped_clusters_result?;
5099
5100 for cluster in shaped_clusters {
5105 let byte_pos = cluster.source_cluster_id.start_byte_in_run as usize;
5106 let orig = byte_ranges.iter().find(|(start, end, ..)| {
5108 byte_pos >= *start && byte_pos < *end
5109 });
5110 let mut cluster = cluster;
5111 if let Some((range_start, _, orig_style, orig_source, orig_nid, orig_mpo)) = orig {
5112 cluster.style = orig_style.clone();
5114 cluster.source_content_index = *orig_source;
5115 cluster.source_node_id = *orig_nid;
5116 cluster.source_cluster_id.source_run = orig_source.run_index;
5118 cluster.source_cluster_id.start_byte_in_run = (byte_pos - range_start) as u32;
5119 for glyph in &mut cluster.glyphs {
5121 glyph.style = orig_style.clone();
5122 }
5123 if let Some(is_outside) = orig_mpo {
5124 cluster.marker_position_outside = Some(*is_outside);
5125 }
5126 }
5127 shaped.push(ShapedItem::Cluster(cluster));
5128 }
5129
5130 idx = coalesce_end;
5131 continue;
5132 }
5133
5134 _total_runs += 1;
5136 _shape_calls += 1;
5137 let direction = if item.bidi_level.is_rtl() {
5138 BidiDirection::Rtl
5139 } else {
5140 BidiDirection::Ltr
5141 };
5142
5143 let language = script_to_language(item.script, &item.text);
5144
5145 let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
5147 FontStack::Ref(font_ref) => {
5148 if let Some(msgs) = debug_messages {
5150 msgs.push(LayoutDebugMessage::info(format!(
5151 "[TextLayout] Using direct FontRef for text: '{}'",
5152 item.text.chars().take(30).collect::<String>()
5153 )));
5154 }
5155 shape_text_correctly(
5156 &item.text,
5157 item.script,
5158 language,
5159 direction,
5160 font_ref,
5161 style,
5162 *source,
5163 *source_node_id,
5164 )
5165 }
5166 FontStack::Stack(selectors) => {
5167 let cache_key = FontChainKey::from_selectors(selectors);
5169
5170 let font_chain = match font_chain_cache.get(&cache_key) {
5172 Some(chain) => chain,
5173 None => {
5174 if let Some(msgs) = debug_messages {
5175 msgs.push(LayoutDebugMessage::warning(format!(
5176 "[TextLayout] Font chain not pre-resolved for {:?} - text will \
5177 not be rendered",
5178 cache_key.font_families
5179 )));
5180 }
5181 idx += 1;
5182 continue;
5183 }
5184 };
5185
5186 let first_char = item.text.chars().next().unwrap_or('A');
5188 let font_id = match font_chain.resolve_char(fc_cache, first_char) {
5189 Some((id, _css_source)) => id,
5190 None => {
5191 if let Some(msgs) = debug_messages {
5192 msgs.push(LayoutDebugMessage::warning(format!(
5193 "[TextLayout] No font in chain can render character '{}' \
5194 (U+{:04X})",
5195 first_char, first_char as u32
5196 )));
5197 }
5198 idx += 1;
5199 continue;
5200 }
5201 };
5202
5203 match loaded_fonts.get(&font_id) {
5205 Some(font) => {
5206 shape_text_correctly(
5207 &item.text,
5208 item.script,
5209 language,
5210 direction,
5211 font,
5212 style,
5213 *source,
5214 *source_node_id,
5215 )
5216 }
5217 None => {
5218 if let Some(msgs) = debug_messages {
5219 let truncated_text = item.text.chars().take(50).collect::<String>();
5220 let display_text = if item.text.chars().count() > 50 {
5221 format!("{}...", truncated_text)
5222 } else {
5223 truncated_text
5224 };
5225
5226 msgs.push(LayoutDebugMessage::warning(format!(
5227 "[TextLayout] Font {:?} not pre-loaded for text: '{}'",
5228 font_id, display_text
5229 )));
5230 }
5231 idx += 1;
5232 continue;
5233 }
5234 }
5235 }
5236 };
5237
5238 let mut shaped_clusters = shaped_clusters_result?;
5239
5240 if let Some(is_outside) = marker_position_outside {
5242 for cluster in &mut shaped_clusters {
5243 cluster.marker_position_outside = Some(*is_outside);
5244 }
5245 }
5246
5247 shaped.extend(shaped_clusters.into_iter().map(ShapedItem::Cluster));
5248 }
5249 LogicalItem::Tab { source, style } => {
5250 let space_advance = style.font_size_px * 0.33;
5254 let tab_width = style.tab_size * space_advance;
5255 shaped.push(ShapedItem::Tab {
5256 source: *source,
5257 bounds: Rect {
5258 x: 0.0,
5259 y: 0.0,
5260 width: tab_width,
5261 height: 0.0,
5262 },
5263 });
5264 }
5265 LogicalItem::Ruby {
5266 source,
5267 base_text,
5268 ruby_text,
5269 style,
5270 } => {
5271 let placeholder_width = base_text.chars().count() as f32 * style.font_size_px * 0.6;
5272 shaped.push(ShapedItem::Object {
5273 source: *source,
5274 bounds: Rect {
5275 x: 0.0,
5276 y: 0.0,
5277 width: placeholder_width,
5278 height: style.line_height * 1.5,
5279 },
5280 baseline_offset: 0.0,
5281 content: InlineContent::Text(StyledRun {
5282 text: base_text.clone(),
5283 style: style.clone(),
5284 logical_start_byte: 0,
5285 source_node_id: None,
5286 }),
5287 });
5288 }
5289 LogicalItem::CombinedText {
5290 style,
5291 source,
5292 text,
5293 } => {
5294 let language = script_to_language(item.script, &item.text);
5295
5296 let glyphs: Vec<Glyph> = match &style.font_stack {
5298 FontStack::Ref(font_ref) => {
5299 if let Some(msgs) = debug_messages {
5301 msgs.push(LayoutDebugMessage::info(format!(
5302 "[TextLayout] Using direct FontRef for CombinedText: '{}'",
5303 text.chars().take(30).collect::<String>()
5304 )));
5305 }
5306 font_ref.shape_text(
5307 text,
5308 item.script,
5309 language,
5310 BidiDirection::Ltr,
5311 style.as_ref(),
5312 )?
5313 }
5314 FontStack::Stack(selectors) => {
5315 let cache_key = FontChainKey::from_selectors(selectors);
5317
5318 let font_chain = match font_chain_cache.get(&cache_key) {
5319 Some(chain) => chain,
5320 None => {
5321 if let Some(msgs) = debug_messages {
5322 msgs.push(LayoutDebugMessage::warning(format!(
5323 "[TextLayout] Font chain not pre-resolved for CombinedText {:?}",
5324 cache_key.font_families
5325 )));
5326 }
5327 idx += 1;
5328 continue;
5329 }
5330 };
5331
5332 let first_char = text.chars().next().unwrap_or('A');
5333 let font_id = match font_chain.resolve_char(fc_cache, first_char) {
5334 Some((id, _)) => id,
5335 None => {
5336 if let Some(msgs) = debug_messages {
5337 msgs.push(LayoutDebugMessage::warning(format!(
5338 "[TextLayout] No font for CombinedText char '{}'",
5339 first_char
5340 )));
5341 }
5342 idx += 1;
5343 continue;
5344 }
5345 };
5346
5347 match loaded_fonts.get(&font_id) {
5348 Some(font) => {
5349 font.shape_text(
5350 text,
5351 item.script,
5352 language,
5353 BidiDirection::Ltr,
5354 style.as_ref(),
5355 )?
5356 }
5357 None => {
5358 if let Some(msgs) = debug_messages {
5359 msgs.push(LayoutDebugMessage::warning(format!(
5360 "[TextLayout] Font {:?} not pre-loaded for CombinedText",
5361 font_id
5362 )));
5363 }
5364 idx += 1;
5365 continue;
5366 }
5367 }
5368 }
5369 };
5370
5371 let shaped_glyphs = glyphs
5372 .into_iter()
5373 .map(|g| ShapedGlyph {
5374 kind: GlyphKind::Character,
5375 glyph_id: g.glyph_id,
5376 script: g.script,
5377 font_hash: g.font_hash,
5378 font_metrics: g.font_metrics,
5379 style: g.style,
5380 cluster_offset: 0,
5381 advance: g.advance,
5382 kerning: g.kerning,
5383 offset: g.offset,
5384 vertical_advance: g.vertical_advance,
5385 vertical_offset: g.vertical_bearing,
5386 })
5387 .collect::<Vec<_>>();
5388
5389 let total_width: f32 = shaped_glyphs.iter().map(|g| g.advance + g.kerning).sum();
5390 let bounds = Rect {
5391 x: 0.0,
5392 y: 0.0,
5393 width: total_width,
5394 height: style.line_height,
5395 };
5396
5397 shaped.push(ShapedItem::CombinedBlock {
5398 source: *source,
5399 glyphs: shaped_glyphs,
5400 bounds,
5401 baseline_offset: 0.0,
5402 });
5403 }
5404 LogicalItem::Object {
5405 content, source, ..
5406 } => {
5407 let (bounds, baseline) = measure_inline_object(content)?;
5408 shaped.push(ShapedItem::Object {
5409 source: *source,
5410 bounds,
5411 baseline_offset: baseline,
5412 content: content.clone(),
5413 });
5414 }
5415 LogicalItem::Break { source, break_info } => {
5416 shaped.push(ShapedItem::Break {
5417 source: *source,
5418 break_info: break_info.clone(),
5419 });
5420 }
5421 }
5422 idx += 1;
5423 }
5424
5425 Ok(shaped)
5426}
5427
5428fn is_hanging_punctuation(item: &ShapedItem) -> bool {
5430 if let ShapedItem::Cluster(c) = item {
5431 if c.glyphs.len() == 1 {
5432 match c.text.as_str() {
5433 "." | "," | ":" | ";" => true,
5434 _ => false,
5435 }
5436 } else {
5437 false
5438 }
5439 } else {
5440 false
5441 }
5442}
5443
5444fn shape_text_correctly<T: ParsedFontTrait>(
5445 text: &str,
5446 script: Script,
5447 language: crate::text3::script::Language,
5448 direction: BidiDirection,
5449 font: &T, style: &Arc<StyleProperties>,
5451 source_index: ContentIndex,
5452 source_node_id: Option<NodeId>,
5453) -> Result<Vec<ShapedCluster>, LayoutError> {
5454 let glyphs = font.shape_text(text, script, language, direction, style.as_ref())?;
5455
5456 if glyphs.is_empty() {
5457 return Ok(Vec::new());
5458 }
5459
5460 let mut clusters = Vec::new();
5461
5462 let mut current_cluster_glyphs = Vec::new();
5464 let mut cluster_id = glyphs[0].cluster;
5465 let mut cluster_start_byte_in_text = glyphs[0].logical_byte_index;
5466
5467 for glyph in glyphs {
5468 if glyph.cluster != cluster_id {
5469 let advance = current_cluster_glyphs
5471 .iter()
5472 .map(|g: &Glyph| g.advance)
5473 .sum();
5474
5475 let (start, end) = if cluster_start_byte_in_text <= glyph.logical_byte_index {
5478 (cluster_start_byte_in_text, glyph.logical_byte_index)
5479 } else {
5480 (glyph.logical_byte_index, cluster_start_byte_in_text)
5481 };
5482 let cluster_text = text.get(start..end).unwrap_or("");
5483
5484 clusters.push(ShapedCluster {
5485 text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
5487 source_run: source_index.run_index,
5488 start_byte_in_run: cluster_id,
5489 },
5490 source_content_index: source_index,
5491 source_node_id,
5492 glyphs: current_cluster_glyphs
5493 .iter()
5494 .map(|g| {
5495 let source_char = text
5496 .get(g.logical_byte_index..)
5497 .and_then(|s| s.chars().next())
5498 .unwrap_or('\u{FFFD}');
5499 let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
5501 (g.logical_byte_index - cluster_start_byte_in_text) as u32
5502 } else {
5503 0
5504 };
5505 ShapedGlyph {
5506 kind: if g.glyph_id == 0 {
5507 GlyphKind::NotDef
5508 } else {
5509 GlyphKind::Character
5510 },
5511 glyph_id: g.glyph_id,
5512 script: g.script,
5513 font_hash: g.font_hash,
5514 font_metrics: g.font_metrics.clone(),
5515 style: g.style.clone(),
5516 cluster_offset,
5517 advance: g.advance,
5518 kerning: g.kerning,
5519 vertical_advance: g.vertical_advance,
5520 vertical_offset: g.vertical_bearing,
5521 offset: g.offset,
5522 }
5523 })
5524 .collect(),
5525 advance,
5526 direction,
5527 style: style.clone(),
5528 marker_position_outside: None,
5529 });
5530 current_cluster_glyphs.clear();
5531 cluster_id = glyph.cluster;
5532 cluster_start_byte_in_text = glyph.logical_byte_index;
5533 }
5534 current_cluster_glyphs.push(glyph);
5535 }
5536
5537 if !current_cluster_glyphs.is_empty() {
5539 let advance = current_cluster_glyphs
5540 .iter()
5541 .map(|g: &Glyph| g.advance)
5542 .sum();
5543 let cluster_text = text.get(cluster_start_byte_in_text..).unwrap_or("");
5544 clusters.push(ShapedCluster {
5545 text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
5547 source_run: source_index.run_index,
5548 start_byte_in_run: cluster_id,
5549 },
5550 source_content_index: source_index,
5551 source_node_id,
5552 glyphs: current_cluster_glyphs
5553 .iter()
5554 .map(|g| {
5555 let source_char = text
5556 .get(g.logical_byte_index..)
5557 .and_then(|s| s.chars().next())
5558 .unwrap_or('\u{FFFD}');
5559 let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
5561 (g.logical_byte_index - cluster_start_byte_in_text) as u32
5562 } else {
5563 0
5564 };
5565 ShapedGlyph {
5566 kind: if g.glyph_id == 0 {
5567 GlyphKind::NotDef
5568 } else {
5569 GlyphKind::Character
5570 },
5571 glyph_id: g.glyph_id,
5572 font_hash: g.font_hash,
5573 font_metrics: g.font_metrics.clone(),
5574 style: g.style.clone(),
5575 script: g.script,
5576 vertical_advance: g.vertical_advance,
5577 vertical_offset: g.vertical_bearing,
5578 cluster_offset,
5579 advance: g.advance,
5580 kerning: g.kerning,
5581 offset: g.offset,
5582 }
5583 })
5584 .collect(),
5585 advance,
5586 direction,
5587 style: style.clone(),
5588 marker_position_outside: None,
5589 });
5590 }
5591
5592 Ok(clusters)
5593}
5594
5595fn measure_inline_object(item: &InlineContent) -> Result<(Rect, f32), LayoutError> {
5597 match item {
5598 InlineContent::Image(img) => {
5599 let size = img.display_size.unwrap_or(img.intrinsic_size);
5600 Ok((
5601 Rect {
5602 x: 0.0,
5603 y: 0.0,
5604 width: size.width,
5605 height: size.height,
5606 },
5607 img.baseline_offset,
5608 ))
5609 }
5610 InlineContent::Shape(shape) => Ok({
5611 let size = shape.shape_def.get_size();
5612 (
5613 Rect {
5614 x: 0.0,
5615 y: 0.0,
5616 width: size.width,
5617 height: size.height,
5618 },
5619 shape.baseline_offset,
5620 )
5621 }),
5622 InlineContent::Space(space) => Ok((
5623 Rect {
5624 x: 0.0,
5625 y: 0.0,
5626 width: space.width,
5627 height: 0.0,
5628 },
5629 0.0,
5630 )),
5631 InlineContent::Marker { .. } => {
5632 Err(LayoutError::InvalidText(
5634 "Marker is text content, not a measurable object".into(),
5635 ))
5636 }
5637 _ => Err(LayoutError::InvalidText("Not a measurable object".into())),
5638 }
5639}
5640
5641fn apply_text_orientation(
5645 items: Arc<Vec<ShapedItem>>,
5646 constraints: &UnifiedConstraints,
5647) -> Result<Arc<Vec<ShapedItem>>, LayoutError> {
5648 if !constraints.is_vertical() {
5649 return Ok(items);
5650 }
5651
5652 let mut oriented_items = Vec::with_capacity(items.len());
5653 let writing_mode = constraints.writing_mode.unwrap_or_default();
5654
5655 for item in items.iter() {
5656 match item {
5657 ShapedItem::Cluster(cluster) => {
5658 let mut new_cluster = cluster.clone();
5659 let mut total_vertical_advance = 0.0;
5660
5661 for glyph in &mut new_cluster.glyphs {
5662 if glyph.vertical_advance > 0.0 {
5665 total_vertical_advance += glyph.vertical_advance;
5666 } else {
5667 let fallback_advance = cluster.style.line_height;
5669 glyph.vertical_advance = fallback_advance;
5670 glyph.vertical_offset = Point {
5672 x: -glyph.advance / 2.0,
5673 y: 0.0,
5674 };
5675 total_vertical_advance += fallback_advance;
5676 }
5677 }
5678 new_cluster.advance = total_vertical_advance;
5680 oriented_items.push(ShapedItem::Cluster(new_cluster));
5681 }
5682 ShapedItem::Object {
5684 source,
5685 bounds,
5686 baseline_offset,
5687 content,
5688 } => {
5689 let mut new_bounds = *bounds;
5690 std::mem::swap(&mut new_bounds.width, &mut new_bounds.height);
5691 oriented_items.push(ShapedItem::Object {
5692 source: *source,
5693 bounds: new_bounds,
5694 baseline_offset: *baseline_offset,
5695 content: content.clone(),
5696 });
5697 }
5698 _ => oriented_items.push(item.clone()),
5699 }
5700 }
5701
5702 Ok(Arc::new(oriented_items))
5703}
5704
5705fn get_item_vertical_align(item: &ShapedItem) -> Option<VerticalAlign> {
5714 match item {
5715 ShapedItem::Object { content, .. } => match content {
5716 InlineContent::Image(img) => Some(img.alignment),
5717 InlineContent::Shape(shape) => Some(shape.alignment),
5718 _ => None,
5719 },
5720 _ => None,
5721 }
5722}
5723
5724pub fn get_item_vertical_metrics(item: &ShapedItem) -> (f32, f32) {
5727 match item {
5729 ShapedItem::Cluster(c) => {
5730 if c.glyphs.is_empty() {
5731 return (c.style.line_height, 0.0);
5733 }
5734 c.glyphs
5737 .iter()
5738 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
5739 let metrics = &glyph.font_metrics;
5740 if metrics.units_per_em == 0 {
5741 return (max_asc, max_desc);
5742 }
5743 let scale = glyph.style.font_size_px / metrics.units_per_em as f32;
5744 let item_asc = metrics.ascent * scale;
5745 let item_desc = (-metrics.descent * scale).max(0.0);
5748 (max_asc.max(item_asc), max_desc.max(item_desc))
5749 })
5750 }
5751 ShapedItem::Object {
5752 bounds,
5753 baseline_offset,
5754 ..
5755 } => {
5756 let ascent = bounds.height - *baseline_offset;
5758 let descent = *baseline_offset;
5759 (ascent.max(0.0), descent.max(0.0))
5760 }
5761 ShapedItem::CombinedBlock {
5762 bounds,
5763 baseline_offset,
5764 ..
5765 } => {
5766 let ascent = bounds.height - *baseline_offset;
5768 let descent = *baseline_offset;
5769 (ascent.max(0.0), descent.max(0.0))
5770 }
5771 _ => (0.0, 0.0), }
5773}
5774
5775fn calculate_line_metrics(items: &[ShapedItem]) -> (f32, f32) {
5778 items
5780 .iter()
5781 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), item| {
5782 let (item_asc, item_desc) = get_item_vertical_metrics(item);
5783 (max_asc.max(item_asc), max_desc.max(item_desc))
5784 })
5785}
5786
5787pub fn perform_fragment_layout<T: ParsedFontTrait>(
5824 cursor: &mut BreakCursor,
5825 logical_items: &[LogicalItem],
5826 fragment_constraints: &UnifiedConstraints,
5827 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
5828 fonts: &LoadedFonts<T>,
5829) -> Result<UnifiedLayout, LayoutError> {
5830 if let Some(msgs) = debug_messages {
5831 msgs.push(LayoutDebugMessage::info(
5832 "\n--- Entering perform_fragment_layout ---".to_string(),
5833 ));
5834 msgs.push(LayoutDebugMessage::info(format!(
5835 "Constraints: available_width={:?}, available_height={:?}, columns={}, text_wrap={:?}",
5836 fragment_constraints.available_width,
5837 fragment_constraints.available_height,
5838 fragment_constraints.columns,
5839 fragment_constraints.text_wrap
5840 )));
5841 }
5842
5843 if fragment_constraints.text_wrap == TextWrap::Balance {
5846 if let Some(msgs) = debug_messages {
5847 msgs.push(LayoutDebugMessage::info(
5848 "Using Knuth-Plass algorithm for text-wrap: balance".to_string(),
5849 ));
5850 }
5851
5852 let shaped_items: Vec<ShapedItem> = cursor.drain_remaining();
5854
5855 let hyphenator = if fragment_constraints.hyphenation {
5856 fragment_constraints
5857 .hyphenation_language
5858 .and_then(|lang| get_hyphenator(lang).ok())
5859 } else {
5860 None
5861 };
5862
5863 return crate::text3::knuth_plass::kp_layout(
5865 &shaped_items,
5866 logical_items,
5867 fragment_constraints,
5868 hyphenator.as_ref(),
5869 fonts,
5870 );
5871 }
5872
5873 let hyphenator = if fragment_constraints.hyphenation {
5874 fragment_constraints
5875 .hyphenation_language
5876 .and_then(|lang| get_hyphenator(lang).ok())
5877 } else {
5878 None
5879 };
5880
5881 let mut positioned_items = Vec::new();
5882 let mut layout_bounds = Rect::default();
5883
5884 let num_columns = fragment_constraints.columns.max(1);
5885 let total_column_gap = fragment_constraints.column_gap * (num_columns - 1) as f32;
5886
5887 let is_min_content = matches!(fragment_constraints.available_width, AvailableSpace::MinContent);
5900 let is_max_content = matches!(fragment_constraints.available_width, AvailableSpace::MaxContent);
5901
5902 let column_width = match fragment_constraints.available_width {
5903 AvailableSpace::Definite(width) => (width - total_column_gap) / num_columns as f32,
5904 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
5905 f32::MAX / 2.0
5908 }
5909 };
5910 let mut current_column = 0;
5911 if let Some(msgs) = debug_messages {
5912 msgs.push(LayoutDebugMessage::info(format!(
5913 "Column width calculated: {}",
5914 column_width
5915 )));
5916 }
5917
5918 let base_direction = fragment_constraints.direction.unwrap_or(BidiDirection::Ltr);
5922
5923 if let Some(msgs) = debug_messages {
5924 msgs.push(LayoutDebugMessage::info(format!(
5925 "[PFLayout] Base direction: {:?} (from CSS), Text align: {:?}",
5926 base_direction, fragment_constraints.text_align
5927 )));
5928 }
5929
5930 'column_loop: while current_column < num_columns {
5931 if let Some(msgs) = debug_messages {
5932 msgs.push(LayoutDebugMessage::info(format!(
5933 "\n-- Starting Column {} --",
5934 current_column
5935 )));
5936 }
5937 let column_start_x =
5938 (column_width + fragment_constraints.column_gap) * current_column as f32;
5939 let mut line_top_y = 0.0;
5940 let mut line_index = 0;
5941 let mut empty_segment_count = 0; const MAX_EMPTY_SEGMENTS: usize = 1000; while !cursor.is_done() {
5945 if let Some(max_height) = fragment_constraints.available_height {
5946 if line_top_y >= max_height {
5947 if let Some(msgs) = debug_messages {
5948 msgs.push(LayoutDebugMessage::info(format!(
5949 " Column full (pen {} >= height {}), breaking to next column.",
5950 line_top_y, max_height
5951 )));
5952 }
5953 break;
5954 }
5955 }
5956
5957 if let Some(clamp) = fragment_constraints.line_clamp {
5958 if line_index >= clamp.get() {
5959 break;
5960 }
5961 }
5962
5963 let mut column_constraints = fragment_constraints.clone();
5965 if is_min_content {
5968 column_constraints.available_width = AvailableSpace::MinContent;
5969 } else if is_max_content {
5970 column_constraints.available_width = AvailableSpace::MaxContent;
5971 } else {
5972 column_constraints.available_width = AvailableSpace::Definite(column_width);
5973 }
5974 let line_constraints = get_line_constraints(
5975 line_top_y,
5976 fragment_constraints.line_height,
5977 &column_constraints,
5978 debug_messages,
5979 );
5980
5981 if line_constraints.segments.is_empty() {
5982 empty_segment_count += 1;
5983 if let Some(msgs) = debug_messages {
5984 msgs.push(LayoutDebugMessage::info(format!(
5985 " No available segments at y={}, skipping to next line. (empty count: \
5986 {}/{})",
5987 line_top_y, empty_segment_count, MAX_EMPTY_SEGMENTS
5988 )));
5989 }
5990
5991 if empty_segment_count >= MAX_EMPTY_SEGMENTS {
5993 if let Some(msgs) = debug_messages {
5994 msgs.push(LayoutDebugMessage::warning(format!(
5995 " [WARN] Reached maximum empty segment count ({}). Breaking to \
5996 prevent infinite loop.",
5997 MAX_EMPTY_SEGMENTS
5998 )));
5999 msgs.push(LayoutDebugMessage::warning(
6000 " This likely means the shape constraints are too restrictive or \
6001 positioned incorrectly."
6002 .to_string(),
6003 ));
6004 msgs.push(LayoutDebugMessage::warning(format!(
6005 " Current y={}, shape boundaries might be outside this range.",
6006 line_top_y
6007 )));
6008 }
6009 break;
6010 }
6011
6012 if !fragment_constraints.shape_boundaries.is_empty() && empty_segment_count > 50 {
6015 let max_shape_y: f32 = fragment_constraints
6017 .shape_boundaries
6018 .iter()
6019 .map(|shape| {
6020 match shape {
6021 ShapeBoundary::Circle { center, radius } => center.y + radius,
6022 ShapeBoundary::Ellipse { center, radii } => center.y + radii.height,
6023 ShapeBoundary::Polygon { points } => {
6024 points.iter().map(|p| p.y).fold(0.0, f32::max)
6025 }
6026 ShapeBoundary::Rectangle(rect) => rect.y + rect.height,
6027 ShapeBoundary::Path { .. } => f32::MAX, }
6029 })
6030 .fold(0.0, f32::max);
6031
6032 if line_top_y > max_shape_y + 100.0 {
6033 if let Some(msgs) = debug_messages {
6034 msgs.push(LayoutDebugMessage::info(format!(
6035 " [INFO] Current y={} is far beyond maximum shape extent y={}. \
6036 Breaking layout.",
6037 line_top_y, max_shape_y
6038 )));
6039 msgs.push(LayoutDebugMessage::info(
6040 " Shape boundaries exist but no segments available - text cannot \
6041 fit in shape."
6042 .to_string(),
6043 ));
6044 }
6045 break;
6046 }
6047 }
6048
6049 line_top_y += fragment_constraints.line_height;
6050 continue;
6051 }
6052
6053 empty_segment_count = 0;
6055
6056 let (mut line_items, was_hyphenated) =
6061 break_one_line(cursor, &line_constraints, false, hyphenator.as_ref(), fonts);
6062 if line_items.is_empty() {
6063 if let Some(msgs) = debug_messages {
6064 msgs.push(LayoutDebugMessage::info(
6065 " Break returned no items. Ending column.".to_string(),
6066 ));
6067 }
6068 break;
6069 }
6070
6071 let line_text_before_rev: String = line_items
6072 .iter()
6073 .filter_map(|i| i.as_cluster())
6074 .map(|c| c.text.as_str())
6075 .collect();
6076 if let Some(msgs) = debug_messages {
6077 msgs.push(LayoutDebugMessage::info(format!(
6078 "[PFLayout] Line items from breaker (visual order): [{}]",
6080 line_text_before_rev
6081 )));
6082 }
6083
6084 let (mut line_pos_items, line_height) = position_one_line(
6085 line_items,
6086 &line_constraints,
6087 line_top_y,
6088 line_index,
6089 fragment_constraints.text_align,
6090 base_direction,
6091 cursor.is_done() && !was_hyphenated,
6092 fragment_constraints,
6093 debug_messages,
6094 fonts,
6095 );
6096
6097 for item in &mut line_pos_items {
6098 item.position.x += column_start_x;
6099 }
6100
6101 line_top_y += line_height.max(fragment_constraints.line_height);
6102 line_index += 1;
6103 positioned_items.extend(line_pos_items);
6104 }
6105 current_column += 1;
6106 }
6107
6108 if let Some(msgs) = debug_messages {
6109 msgs.push(LayoutDebugMessage::info(format!(
6110 "--- Exiting perform_fragment_layout, positioned {} items ---",
6111 positioned_items.len()
6112 )));
6113 }
6114
6115 let layout = UnifiedLayout {
6116 items: positioned_items,
6117 overflow: OverflowInfo::default(),
6118 };
6119
6120 let calculated_bounds = layout.bounds();
6122
6123 if let Some(msgs) = debug_messages {
6124 msgs.push(LayoutDebugMessage::info(format!(
6125 "--- Calculated bounds: width={}, height={} ---",
6126 calculated_bounds.width, calculated_bounds.height
6127 )));
6128 }
6129
6130 Ok(layout)
6131}
6132
6133pub fn break_one_line<T: ParsedFontTrait>(
6184 cursor: &mut BreakCursor,
6185 line_constraints: &LineConstraints,
6186 is_vertical: bool,
6187 hyphenator: Option<&Standard>,
6188 fonts: &LoadedFonts<T>,
6189) -> (Vec<ShapedItem>, bool) {
6190 let mut line_items = Vec::new();
6191 let mut current_width = 0.0;
6192
6193 if cursor.is_done() {
6194 return (Vec::new(), false);
6195 }
6196
6197 while !cursor.is_done() {
6201 let next_unit = cursor.peek_next_unit();
6202 if next_unit.is_empty() {
6203 break;
6204 }
6205 if next_unit.len() == 1 && is_word_separator(&next_unit[0]) {
6207 cursor.consume(1);
6209 } else {
6210 break;
6211 }
6212 }
6213
6214 loop {
6215 let next_unit = cursor.peek_next_unit();
6217 if next_unit.is_empty() {
6218 break; }
6220
6221 if let Some(ShapedItem::Break { .. }) = next_unit.first() {
6223 line_items.push(next_unit[0].clone());
6224 cursor.consume(1);
6225 return (line_items, false);
6226 }
6227
6228 let unit_width: f32 = next_unit
6229 .iter()
6230 .map(|item| get_item_measure(item, is_vertical))
6231 .sum();
6232 let available_width = line_constraints.total_available - current_width;
6233
6234 if unit_width <= available_width {
6236 line_items.extend_from_slice(&next_unit);
6237 current_width += unit_width;
6238 cursor.consume(next_unit.len());
6239 } else {
6240 if let Some(hyphenator) = hyphenator {
6242 if !is_break_opportunity(next_unit.last().unwrap()) {
6244 if let Some(hyphenation_result) = try_hyphenate_word_cluster(
6245 &next_unit,
6246 available_width,
6247 is_vertical,
6248 hyphenator,
6249 fonts,
6250 ) {
6251 line_items.extend(hyphenation_result.line_part);
6252 cursor.consume(next_unit.len());
6254 cursor.partial_remainder = hyphenation_result.remainder_part;
6256 return (line_items, true);
6257 }
6258 }
6259 }
6260
6261 if line_items.is_empty() {
6264 line_items.push(next_unit[0].clone());
6265 cursor.consume(1);
6266 }
6267 break;
6268 }
6269 }
6270
6271 (line_items, false)
6272}
6273
6274#[derive(Clone)]
6276pub struct HyphenationBreak {
6277 pub char_len_on_line: usize,
6279 pub width_on_line: f32,
6281 pub line_part: Vec<ShapedItem>,
6283 pub hyphen_item: ShapedItem,
6285 pub remainder_part: Vec<ShapedItem>,
6288}
6289
6290pub fn find_all_hyphenation_breaks<T: ParsedFontTrait>(
6292 word_clusters: &[ShapedCluster],
6293 hyphenator: &Standard,
6294 is_vertical: bool, fonts: &LoadedFonts<T>,
6296) -> Option<Vec<HyphenationBreak>> {
6297 if word_clusters.is_empty() {
6298 return None;
6299 }
6300
6301 let mut word_string = String::new();
6303 let mut char_map = Vec::new();
6304 let mut current_width = 0.0;
6305
6306 for (cluster_idx, cluster) in word_clusters.iter().enumerate() {
6307 for (char_byte_offset, _ch) in cluster.text.char_indices() {
6308 let glyph_idx = cluster
6309 .glyphs
6310 .iter()
6311 .rposition(|g| g.cluster_offset as usize <= char_byte_offset)
6312 .unwrap_or(0);
6313 let glyph = &cluster.glyphs[glyph_idx];
6314
6315 let num_chars_in_glyph = cluster.text[glyph.cluster_offset as usize..]
6316 .chars()
6317 .count();
6318 let advance_per_char = if is_vertical {
6319 glyph.vertical_advance
6320 } else {
6321 glyph.advance
6322 } / (num_chars_in_glyph as f32).max(1.0);
6323
6324 current_width += advance_per_char;
6325 char_map.push((cluster_idx, glyph_idx, current_width));
6326 }
6327 word_string.push_str(&cluster.text);
6328 }
6329
6330 let opportunities = hyphenator.hyphenate(&word_string);
6332 if opportunities.breaks.is_empty() {
6333 return None;
6334 }
6335
6336 let last_cluster = word_clusters.last().unwrap();
6337 let last_glyph = last_cluster.glyphs.last().unwrap();
6338 let style = last_cluster.style.clone();
6339
6340 let font = fonts.get_by_hash(last_glyph.font_hash)?;
6342 let (hyphen_glyph_id, hyphen_advance) =
6343 font.get_hyphen_glyph_and_advance(style.font_size_px)?;
6344
6345 let mut possible_breaks = Vec::new();
6346
6347 for &break_char_idx in &opportunities.breaks {
6349 if break_char_idx == 0 || break_char_idx > char_map.len() {
6352 continue;
6353 }
6354
6355 let (_, _, width_at_break) = char_map[break_char_idx - 1];
6356
6357 let line_part: Vec<ShapedItem> = word_clusters[..break_char_idx]
6359 .iter()
6360 .map(|c| ShapedItem::Cluster(c.clone()))
6361 .collect();
6362
6363 let remainder_part: Vec<ShapedItem> = word_clusters[break_char_idx..]
6365 .iter()
6366 .map(|c| ShapedItem::Cluster(c.clone()))
6367 .collect();
6368
6369 let hyphen_item = ShapedItem::Cluster(ShapedCluster {
6370 text: "-".to_string(),
6371 source_cluster_id: GraphemeClusterId {
6372 source_run: u32::MAX,
6373 start_byte_in_run: u32::MAX,
6374 },
6375 source_content_index: ContentIndex {
6376 run_index: u32::MAX,
6377 item_index: u32::MAX,
6378 },
6379 source_node_id: None, glyphs: vec![ShapedGlyph {
6381 kind: GlyphKind::Hyphen,
6382 glyph_id: hyphen_glyph_id,
6383 font_hash: last_glyph.font_hash,
6384 font_metrics: last_glyph.font_metrics.clone(),
6385 cluster_offset: 0,
6386 script: Script::Latin,
6387 advance: hyphen_advance,
6388 kerning: 0.0,
6389 offset: Point::default(),
6390 style: style.clone(),
6391 vertical_advance: hyphen_advance,
6392 vertical_offset: Point::default(),
6393 }],
6394 advance: hyphen_advance,
6395 direction: BidiDirection::Ltr,
6396 style: style.clone(),
6397 marker_position_outside: None,
6398 });
6399
6400 possible_breaks.push(HyphenationBreak {
6401 char_len_on_line: break_char_idx,
6402 width_on_line: width_at_break + hyphen_advance,
6403 line_part,
6404 hyphen_item,
6405 remainder_part,
6406 });
6407 }
6408
6409 Some(possible_breaks)
6410}
6411
6412fn try_hyphenate_word_cluster<T: ParsedFontTrait>(
6414 word_items: &[ShapedItem],
6415 remaining_width: f32,
6416 is_vertical: bool,
6417 hyphenator: &Standard,
6418 fonts: &LoadedFonts<T>,
6419) -> Option<HyphenationResult> {
6420 let word_clusters: Vec<ShapedCluster> = word_items
6421 .iter()
6422 .filter_map(|item| item.as_cluster().cloned())
6423 .collect();
6424
6425 if word_clusters.is_empty() {
6426 return None;
6427 }
6428
6429 let all_breaks = find_all_hyphenation_breaks(&word_clusters, hyphenator, is_vertical, fonts)?;
6430
6431 if let Some(best_break) = all_breaks
6432 .into_iter()
6433 .rfind(|b| b.width_on_line <= remaining_width)
6434 {
6435 let mut line_part = best_break.line_part;
6436 line_part.push(best_break.hyphen_item);
6437
6438 return Some(HyphenationResult {
6439 line_part,
6440 remainder_part: best_break.remainder_part,
6441 });
6442 }
6443
6444 None
6445}
6446
6447pub fn position_one_line<T: ParsedFontTrait>(
6518 line_items: Vec<ShapedItem>,
6519 line_constraints: &LineConstraints,
6520 line_top_y: f32,
6521 line_index: usize,
6522 text_align: TextAlign,
6523 base_direction: BidiDirection,
6524 is_last_line: bool,
6525 constraints: &UnifiedConstraints,
6526 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6527 fonts: &LoadedFonts<T>,
6528) -> (Vec<PositionedItem>, f32) {
6529 let line_text: String = line_items
6530 .iter()
6531 .filter_map(|i| i.as_cluster())
6532 .map(|c| c.text.as_str())
6533 .collect();
6534 if let Some(msgs) = debug_messages {
6535 msgs.push(LayoutDebugMessage::info(format!(
6536 "\n--- Entering position_one_line for line: [{}] ---",
6537 line_text
6538 )));
6539 }
6540 let physical_align = match (text_align, base_direction) {
6542 (TextAlign::Start, BidiDirection::Ltr) => TextAlign::Left,
6543 (TextAlign::Start, BidiDirection::Rtl) => TextAlign::Right,
6544 (TextAlign::End, BidiDirection::Ltr) => TextAlign::Right,
6545 (TextAlign::End, BidiDirection::Rtl) => TextAlign::Left,
6546 (other, _) => other,
6548 };
6549 if let Some(msgs) = debug_messages {
6550 msgs.push(LayoutDebugMessage::info(format!(
6551 "[Pos1Line] Physical align: {:?}",
6552 physical_align
6553 )));
6554 }
6555
6556 if line_items.is_empty() {
6557 return (Vec::new(), 0.0);
6558 }
6559 let mut positioned = Vec::new();
6560 let is_vertical = constraints.is_vertical();
6561
6562 let (line_ascent, line_descent) = calculate_line_metrics(&line_items);
6564 let line_box_height = line_ascent + line_descent;
6565
6566 let line_baseline_y = line_top_y + line_ascent;
6568
6569 let mut item_cursor = 0;
6571 let is_first_line_of_para = line_index == 0; for (segment_idx, segment) in line_constraints.segments.iter().enumerate() {
6574 if item_cursor >= line_items.len() {
6575 break;
6576 }
6577
6578 let mut segment_items = Vec::new();
6580 let mut current_segment_width = 0.0;
6581 while item_cursor < line_items.len() {
6582 let item = &line_items[item_cursor];
6583 let item_measure = get_item_measure(item, is_vertical);
6584 if current_segment_width + item_measure > segment.width && !segment_items.is_empty() {
6586 break;
6587 }
6588 segment_items.push(item.clone());
6589 current_segment_width += item_measure;
6590 item_cursor += 1;
6591 }
6592
6593 if segment_items.is_empty() {
6594 continue;
6595 }
6596
6597 let (extra_word_spacing, extra_char_spacing) = if constraints.text_justify
6599 != JustifyContent::None
6600 && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
6601 && constraints.text_justify != JustifyContent::Kashida
6602 {
6603 let segment_line_constraints = LineConstraints {
6604 segments: vec![segment.clone()],
6605 total_available: segment.width,
6606 };
6607 calculate_justification_spacing(
6608 &segment_items,
6609 &segment_line_constraints,
6610 constraints.text_justify,
6611 is_vertical,
6612 )
6613 } else {
6614 (0.0, 0.0)
6615 };
6616
6617 let justified_segment_items = if constraints.text_justify == JustifyContent::Kashida
6619 && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
6620 {
6621 let segment_line_constraints = LineConstraints {
6622 segments: vec![segment.clone()],
6623 total_available: segment.width,
6624 };
6625 justify_kashida_and_rebuild(
6626 segment_items,
6627 &segment_line_constraints,
6628 is_vertical,
6629 debug_messages,
6630 fonts,
6631 )
6632 } else {
6633 segment_items
6634 };
6635
6636 let final_segment_width: f32 = justified_segment_items
6638 .iter()
6639 .map(|item| get_item_measure(item, is_vertical))
6640 .sum();
6641
6642 let remaining_space = segment.width - final_segment_width;
6644
6645 let is_indefinite_width = segment.width.is_infinite() || segment.width > 1e30;
6651 let alignment_offset = if is_indefinite_width {
6652 0.0 } else {
6654 match physical_align {
6655 TextAlign::Center => remaining_space / 2.0,
6656 TextAlign::Right => remaining_space,
6657 _ => 0.0, }
6659 };
6660
6661 let mut main_axis_pen = segment.start_x + alignment_offset;
6662 if let Some(msgs) = debug_messages {
6663 msgs.push(LayoutDebugMessage::info(format!(
6664 "[Pos1Line] Segment width: {}, Item width: {}, Remaining space: {}, Initial pen: \
6665 {}",
6666 segment.width, final_segment_width, remaining_space, main_axis_pen
6667 )));
6668 }
6669
6670 if is_first_line_of_para && segment_idx == 0 {
6672 main_axis_pen += constraints.text_indent;
6673 }
6674
6675 let total_marker_width: f32 = justified_segment_items
6678 .iter()
6679 .filter_map(|item| {
6680 if let ShapedItem::Cluster(c) = item {
6681 if c.marker_position_outside == Some(true) {
6682 return Some(get_item_measure(item, is_vertical));
6683 }
6684 }
6685 None
6686 })
6687 .sum();
6688
6689 let marker_spacing = 4.0; let mut marker_pen = if total_marker_width > 0.0 {
6692 -(total_marker_width + marker_spacing)
6693 } else {
6694 0.0
6695 };
6696
6697 for item in justified_segment_items {
6710 let (item_ascent, item_descent) = get_item_vertical_metrics(&item);
6711 let effective_align = get_item_vertical_align(&item)
6713 .unwrap_or(constraints.vertical_align);
6714 let item_baseline_pos = match effective_align {
6715 VerticalAlign::Top => line_top_y + item_ascent,
6716 VerticalAlign::Middle => {
6717 line_top_y + (line_box_height / 2.0) - ((item_ascent + item_descent) / 2.0)
6718 + item_ascent
6719 }
6720 VerticalAlign::Bottom => line_top_y + line_box_height - item_descent,
6721 _ => line_baseline_y, };
6723
6724 let item_measure = get_item_measure(&item, is_vertical);
6726
6727 let position = if is_vertical {
6728 Point {
6729 x: item_baseline_pos - item_ascent,
6730 y: main_axis_pen,
6731 }
6732 } else {
6733 if let Some(msgs) = debug_messages {
6734 msgs.push(LayoutDebugMessage::info(format!(
6735 "[Pos1Line] is_vertical=false, main_axis_pen={}, item_baseline_pos={}, \
6736 item_ascent={}",
6737 main_axis_pen, item_baseline_pos, item_ascent
6738 )));
6739 }
6740
6741 let x_position = if let ShapedItem::Cluster(cluster) = &item {
6743 if cluster.marker_position_outside == Some(true) {
6744 let marker_width = item_measure;
6746 if let Some(msgs) = debug_messages {
6747 msgs.push(LayoutDebugMessage::info(format!(
6748 "[Pos1Line] Outside marker detected! width={}, positioning at \
6749 marker_pen={}",
6750 marker_width, marker_pen
6751 )));
6752 }
6753 let pos = marker_pen;
6754 marker_pen += marker_width; pos
6756 } else {
6757 main_axis_pen
6758 }
6759 } else {
6760 main_axis_pen
6761 };
6762
6763 Point {
6764 y: item_baseline_pos - item_ascent,
6765 x: x_position,
6766 }
6767 };
6768
6769 let item_text = item
6771 .as_cluster()
6772 .map(|c| c.text.as_str())
6773 .unwrap_or("[OBJ]");
6774 if let Some(msgs) = debug_messages {
6775 msgs.push(LayoutDebugMessage::info(format!(
6776 "[Pos1Line] Positioning item '{}' at pen_x={}",
6777 item_text, main_axis_pen
6778 )));
6779 }
6780 positioned.push(PositionedItem {
6781 item: item.clone(),
6782 position,
6783 line_index,
6784 });
6785
6786 let is_outside_marker = if let ShapedItem::Cluster(c) = &item {
6788 c.marker_position_outside == Some(true)
6789 } else {
6790 false
6791 };
6792
6793 if !is_outside_marker {
6794 main_axis_pen += item_measure;
6795 }
6796
6797 if !is_outside_marker && extra_char_spacing > 0.0 && can_justify_after(&item) {
6799 main_axis_pen += extra_char_spacing;
6800 }
6801 if let ShapedItem::Cluster(c) = &item {
6802 if !is_outside_marker {
6803 let letter_spacing_px = match c.style.letter_spacing {
6804 Spacing::Px(px) => px as f32,
6805 Spacing::Em(em) => em * c.style.font_size_px,
6806 };
6807 main_axis_pen += letter_spacing_px;
6808 if is_word_separator(&item) {
6809 let word_spacing_px = match c.style.word_spacing {
6810 Spacing::Px(px) => px as f32,
6811 Spacing::Em(em) => em * c.style.font_size_px,
6812 };
6813 main_axis_pen += word_spacing_px;
6814 main_axis_pen += extra_word_spacing;
6815 }
6816 }
6817 }
6818 }
6819 }
6820
6821 (positioned, line_box_height)
6822}
6823
6824fn calculate_alignment_offset(
6826 items: &[ShapedItem],
6827 line_constraints: &LineConstraints,
6828 align: TextAlign,
6829 is_vertical: bool,
6830 constraints: &UnifiedConstraints,
6831) -> f32 {
6832 if let Some(segment) = line_constraints.segments.first() {
6834 let total_width: f32 = items
6835 .iter()
6836 .map(|item| get_item_measure(item, is_vertical))
6837 .sum();
6838
6839 let available_width = if constraints.segment_alignment == SegmentAlignment::Total {
6840 line_constraints.total_available
6841 } else {
6842 segment.width
6843 };
6844
6845 if total_width >= available_width {
6846 return 0.0; }
6848
6849 let remaining_space = available_width - total_width;
6850
6851 match align {
6852 TextAlign::Center => remaining_space / 2.0,
6853 TextAlign::Right => remaining_space,
6854 _ => 0.0, }
6856 } else {
6857 0.0
6858 }
6859}
6860
6861fn calculate_justification_spacing(
6876 items: &[ShapedItem],
6877 line_constraints: &LineConstraints,
6878 text_justify: JustifyContent,
6879 is_vertical: bool,
6880) -> (f32, f32) {
6881 let total_width: f32 = items
6883 .iter()
6884 .map(|item| get_item_measure(item, is_vertical))
6885 .sum();
6886 let available_width = line_constraints.total_available;
6887
6888 if total_width >= available_width || available_width <= 0.0 {
6889 return (0.0, 0.0);
6890 }
6891
6892 let extra_space = available_width - total_width;
6893
6894 match text_justify {
6895 JustifyContent::InterWord => {
6896 let space_count = items.iter().filter(|item| is_word_separator(item)).count();
6898 if space_count > 0 {
6899 (extra_space / space_count as f32, 0.0)
6900 } else {
6901 (0.0, 0.0) }
6903 }
6904 JustifyContent::InterCharacter | JustifyContent::Distribute => {
6905 let gap_count = items
6907 .iter()
6908 .enumerate()
6909 .filter(|(i, item)| *i < items.len() - 1 && can_justify_after(item))
6910 .count();
6911 if gap_count > 0 {
6912 (0.0, extra_space / gap_count as f32)
6913 } else {
6914 (0.0, 0.0) }
6916 }
6917 _ => (0.0, 0.0),
6919 }
6920}
6921
6922pub fn justify_kashida_and_rebuild<T: ParsedFontTrait>(
6928 items: Vec<ShapedItem>,
6929 line_constraints: &LineConstraints,
6930 is_vertical: bool,
6931 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6932 fonts: &LoadedFonts<T>,
6933) -> Vec<ShapedItem> {
6934 if let Some(msgs) = debug_messages {
6935 msgs.push(LayoutDebugMessage::info(
6936 "\n--- Entering justify_kashida_and_rebuild ---".to_string(),
6937 ));
6938 }
6939 let total_width: f32 = items
6940 .iter()
6941 .map(|item| get_item_measure(item, is_vertical))
6942 .sum();
6943 let available_width = line_constraints.total_available;
6944 if let Some(msgs) = debug_messages {
6945 msgs.push(LayoutDebugMessage::info(format!(
6946 "Total item width: {}, Available width: {}",
6947 total_width, available_width
6948 )));
6949 }
6950
6951 if total_width >= available_width || available_width <= 0.0 {
6952 if let Some(msgs) = debug_messages {
6953 msgs.push(LayoutDebugMessage::info(
6954 "No justification needed (line is full or invalid).".to_string(),
6955 ));
6956 }
6957 return items;
6958 }
6959
6960 let extra_space = available_width - total_width;
6961 if let Some(msgs) = debug_messages {
6962 msgs.push(LayoutDebugMessage::info(format!(
6963 "Extra space to fill: {}",
6964 extra_space
6965 )));
6966 }
6967
6968 let font_info = items.iter().find_map(|item| {
6969 if let ShapedItem::Cluster(c) = item {
6970 if let Some(glyph) = c.glyphs.first() {
6971 if glyph.script == Script::Arabic {
6972 if let Some(font) = fonts.get_by_hash(glyph.font_hash) {
6974 return Some((
6975 font.clone(),
6976 glyph.font_hash,
6977 glyph.font_metrics.clone(),
6978 glyph.style.clone(),
6979 ));
6980 }
6981 }
6982 }
6983 }
6984 None
6985 });
6986
6987 let (font, font_hash, font_metrics, style) = match font_info {
6988 Some(info) => {
6989 if let Some(msgs) = debug_messages {
6990 msgs.push(LayoutDebugMessage::info(
6991 "Found Arabic font for kashida.".to_string(),
6992 ));
6993 }
6994 info
6995 }
6996 None => {
6997 if let Some(msgs) = debug_messages {
6998 msgs.push(LayoutDebugMessage::info(
6999 "No Arabic font found on line. Cannot insert kashidas.".to_string(),
7000 ));
7001 }
7002 return items;
7003 }
7004 };
7005
7006 let (kashida_glyph_id, kashida_advance) =
7007 match font.get_kashida_glyph_and_advance(style.font_size_px) {
7008 Some((id, adv)) if adv > 0.0 => {
7009 if let Some(msgs) = debug_messages {
7010 msgs.push(LayoutDebugMessage::info(format!(
7011 "Font provides kashida glyph with advance {}",
7012 adv
7013 )));
7014 }
7015 (id, adv)
7016 }
7017 _ => {
7018 if let Some(msgs) = debug_messages {
7019 msgs.push(LayoutDebugMessage::info(
7020 "Font does not support kashida justification.".to_string(),
7021 ));
7022 }
7023 return items;
7024 }
7025 };
7026
7027 let opportunity_indices: Vec<usize> = items
7028 .windows(2)
7029 .enumerate()
7030 .filter_map(|(i, window)| {
7031 if let (ShapedItem::Cluster(cur), ShapedItem::Cluster(next)) = (&window[0], &window[1])
7032 {
7033 if is_arabic_cluster(cur)
7034 && is_arabic_cluster(next)
7035 && !is_word_separator(&window[1])
7036 {
7037 return Some(i + 1);
7038 }
7039 }
7040 None
7041 })
7042 .collect();
7043
7044 if let Some(msgs) = debug_messages {
7045 msgs.push(LayoutDebugMessage::info(format!(
7046 "Found {} kashida insertion opportunities at indices: {:?}",
7047 opportunity_indices.len(),
7048 opportunity_indices
7049 )));
7050 }
7051
7052 if opportunity_indices.is_empty() {
7053 if let Some(msgs) = debug_messages {
7054 msgs.push(LayoutDebugMessage::info(
7055 "No opportunities found. Exiting.".to_string(),
7056 ));
7057 }
7058 return items;
7059 }
7060
7061 let num_kashidas_to_insert = (extra_space / kashida_advance).floor() as usize;
7062 if let Some(msgs) = debug_messages {
7063 msgs.push(LayoutDebugMessage::info(format!(
7064 "Calculated number of kashidas to insert: {}",
7065 num_kashidas_to_insert
7066 )));
7067 }
7068
7069 if num_kashidas_to_insert == 0 {
7070 return items;
7071 }
7072
7073 let kashidas_per_point = num_kashidas_to_insert / opportunity_indices.len();
7074 let mut remainder = num_kashidas_to_insert % opportunity_indices.len();
7075 if let Some(msgs) = debug_messages {
7076 msgs.push(LayoutDebugMessage::info(format!(
7077 "Distributing kashidas: {} per point, with {} remainder.",
7078 kashidas_per_point, remainder
7079 )));
7080 }
7081
7082 let kashida_item = {
7083 let kashida_glyph = ShapedGlyph {
7085 kind: GlyphKind::Kashida {
7086 width: kashida_advance,
7087 },
7088 glyph_id: kashida_glyph_id,
7089 font_hash,
7090 font_metrics: font_metrics.clone(),
7091 style: style.clone(),
7092 script: Script::Arabic,
7093 advance: kashida_advance,
7094 kerning: 0.0,
7095 cluster_offset: 0,
7096 offset: Point::default(),
7097 vertical_advance: 0.0,
7098 vertical_offset: Point::default(),
7099 };
7100 ShapedItem::Cluster(ShapedCluster {
7101 text: "\u{0640}".to_string(),
7102 source_cluster_id: GraphemeClusterId {
7103 source_run: u32::MAX,
7104 start_byte_in_run: u32::MAX,
7105 },
7106 source_content_index: ContentIndex {
7107 run_index: u32::MAX,
7108 item_index: u32::MAX,
7109 },
7110 source_node_id: None, glyphs: vec![kashida_glyph],
7112 advance: kashida_advance,
7113 direction: BidiDirection::Ltr,
7114 style,
7115 marker_position_outside: None,
7116 })
7117 };
7118
7119 let mut new_items = Vec::with_capacity(items.len() + num_kashidas_to_insert);
7120 let mut last_copy_idx = 0;
7121 for &point in &opportunity_indices {
7122 new_items.extend_from_slice(&items[last_copy_idx..point]);
7123 let mut num_to_insert = kashidas_per_point;
7124 if remainder > 0 {
7125 num_to_insert += 1;
7126 remainder -= 1;
7127 }
7128 for _ in 0..num_to_insert {
7129 new_items.push(kashida_item.clone());
7130 }
7131 last_copy_idx = point;
7132 }
7133 new_items.extend_from_slice(&items[last_copy_idx..]);
7134
7135 if let Some(msgs) = debug_messages {
7136 msgs.push(LayoutDebugMessage::info(format!(
7137 "--- Exiting justify_kashida_and_rebuild, new item count: {} ---",
7138 new_items.len()
7139 )));
7140 }
7141 new_items
7142}
7143
7144fn is_arabic_cluster(cluster: &ShapedCluster) -> bool {
7146 cluster.glyphs.iter().any(|g| g.script == Script::Arabic)
7149}
7150
7151pub fn is_word_separator(item: &ShapedItem) -> bool {
7153 if let ShapedItem::Cluster(c) = item {
7154 c.text.chars().any(|g| g.is_whitespace())
7157 } else {
7158 false
7159 }
7160}
7161
7162fn can_justify_after(item: &ShapedItem) -> bool {
7164 if let ShapedItem::Cluster(c) = item {
7165 c.text.chars().last().map_or(false, |g| {
7166 !g.is_whitespace() && classify_character(g as u32) != CharacterClass::Combining
7167 })
7168 } else {
7169 !matches!(item, ShapedItem::Break { .. })
7171 }
7172}
7173
7174fn classify_character(codepoint: u32) -> CharacterClass {
7177 match codepoint {
7178 0x0020 | 0x00A0 | 0x3000 => CharacterClass::Space,
7179 0x0021..=0x002F | 0x003A..=0x0040 | 0x005B..=0x0060 | 0x007B..=0x007E => {
7180 CharacterClass::Punctuation
7181 }
7182 0x4E00..=0x9FFF | 0x3400..=0x4DBF => CharacterClass::Ideograph,
7183 0x0300..=0x036F | 0x1AB0..=0x1AFF => CharacterClass::Combining,
7184 0x1800..=0x18AF => CharacterClass::Letter,
7186 _ => CharacterClass::Letter,
7187 }
7188}
7189
7190pub fn get_item_measure(item: &ShapedItem, is_vertical: bool) -> f32 {
7192 match item {
7193 ShapedItem::Cluster(c) => {
7194 let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
7198 c.advance + total_kerning
7199 }
7200 ShapedItem::Object { bounds, .. }
7201 | ShapedItem::CombinedBlock { bounds, .. }
7202 | ShapedItem::Tab { bounds, .. } => {
7203 if is_vertical {
7204 bounds.height
7205 } else {
7206 bounds.width
7207 }
7208 }
7209 ShapedItem::Break { .. } => 0.0,
7210 }
7211}
7212
7213fn get_item_bounds(item: &PositionedItem) -> Rect {
7215 let measure = get_item_measure(&item.item, false); let cross_measure = match &item.item {
7217 ShapedItem::Object { bounds, .. } => bounds.height,
7218 _ => 20.0, };
7220 Rect {
7221 x: item.position.x,
7222 y: item.position.y,
7223 width: measure,
7224 height: cross_measure,
7225 }
7226}
7227
7228fn get_line_constraints(
7231 line_y: f32,
7232 line_height: f32,
7233 constraints: &UnifiedConstraints,
7234 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
7235) -> LineConstraints {
7236 if let Some(msgs) = debug_messages {
7237 msgs.push(LayoutDebugMessage::info(format!(
7238 "\n--- Entering get_line_constraints for y={} ---",
7239 line_y
7240 )));
7241 }
7242
7243 let mut available_segments = Vec::new();
7244 if constraints.shape_boundaries.is_empty() {
7245 let segment_width = match constraints.available_width {
7255 AvailableSpace::Definite(w) => w, AvailableSpace::MaxContent => f32::MAX / 2.0, AvailableSpace::MinContent => f32::MAX / 2.0, };
7259 available_segments.push(LineSegment {
7262 start_x: 0.0,
7263 width: segment_width,
7264 priority: 0,
7265 });
7266 } else {
7267 }
7269
7270 if let Some(msgs) = debug_messages {
7271 msgs.push(LayoutDebugMessage::info(format!(
7272 "Initial available segments: {:?}",
7273 available_segments
7274 )));
7275 }
7276
7277 for (idx, exclusion) in constraints.shape_exclusions.iter().enumerate() {
7278 if let Some(msgs) = debug_messages {
7279 msgs.push(LayoutDebugMessage::info(format!(
7280 "Applying exclusion #{}: {:?}",
7281 idx, exclusion
7282 )));
7283 }
7284 let exclusion_spans =
7285 get_shape_horizontal_spans(exclusion, line_y, line_height).unwrap_or_default();
7286 if let Some(msgs) = debug_messages {
7287 msgs.push(LayoutDebugMessage::info(format!(
7288 " Exclusion spans at y={}: {:?}",
7289 line_y, exclusion_spans
7290 )));
7291 }
7292
7293 if exclusion_spans.is_empty() {
7294 continue;
7295 }
7296
7297 let mut next_segments = Vec::new();
7298 for (excl_start, excl_end) in exclusion_spans {
7299 for segment in &available_segments {
7300 let seg_start = segment.start_x;
7301 let seg_end = segment.start_x + segment.width;
7302
7303 if seg_end > excl_start && seg_start < excl_end {
7305 if seg_start < excl_start {
7306 next_segments.push(LineSegment {
7308 start_x: seg_start,
7309 width: excl_start - seg_start,
7310 priority: segment.priority,
7311 });
7312 }
7313 if seg_end > excl_end {
7314 next_segments.push(LineSegment {
7316 start_x: excl_end,
7317 width: seg_end - excl_end,
7318 priority: segment.priority,
7319 });
7320 }
7321 } else {
7322 next_segments.push(segment.clone()); }
7324 }
7325 available_segments = merge_segments(next_segments);
7326 next_segments = Vec::new();
7327 }
7328 if let Some(msgs) = debug_messages {
7329 msgs.push(LayoutDebugMessage::info(format!(
7330 " Segments after exclusion #{}: {:?}",
7331 idx, available_segments
7332 )));
7333 }
7334 }
7335
7336 let total_width = available_segments.iter().map(|s| s.width).sum();
7337 if let Some(msgs) = debug_messages {
7338 msgs.push(LayoutDebugMessage::info(format!(
7339 "Final segments: {:?}, total available width: {}",
7340 available_segments, total_width
7341 )));
7342 msgs.push(LayoutDebugMessage::info(
7343 "--- Exiting get_line_constraints ---".to_string(),
7344 ));
7345 }
7346
7347 LineConstraints {
7348 segments: available_segments,
7349 total_available: total_width,
7350 }
7351}
7352
7353fn get_shape_horizontal_spans(
7356 shape: &ShapeBoundary,
7357 y: f32,
7358 line_height: f32,
7359) -> Result<Vec<(f32, f32)>, LayoutError> {
7360 match shape {
7361 ShapeBoundary::Rectangle(rect) => {
7362 let line_start = y;
7365 let line_end = y + line_height;
7366 let rect_start = rect.y;
7367 let rect_end = rect.y + rect.height;
7368
7369 if line_start < rect_end && line_end > rect_start {
7370 Ok(vec![(rect.x, rect.x + rect.width)])
7371 } else {
7372 Ok(vec![])
7373 }
7374 }
7375 ShapeBoundary::Circle { center, radius } => {
7376 let line_center_y = y + line_height / 2.0;
7377 let dy = (line_center_y - center.y).abs();
7378 if dy <= *radius {
7379 let dx = (radius.powi(2) - dy.powi(2)).sqrt();
7380 Ok(vec![(center.x - dx, center.x + dx)])
7381 } else {
7382 Ok(vec![])
7383 }
7384 }
7385 ShapeBoundary::Ellipse { center, radii } => {
7386 let line_center_y = y + line_height / 2.0;
7387 let dy = line_center_y - center.y;
7388 if dy.abs() <= radii.height {
7389 let y_term = dy / radii.height;
7391 let x_term_squared = 1.0 - y_term.powi(2);
7392 if x_term_squared >= 0.0 {
7393 let dx = radii.width * x_term_squared.sqrt();
7394 Ok(vec![(center.x - dx, center.x + dx)])
7395 } else {
7396 Ok(vec![])
7397 }
7398 } else {
7399 Ok(vec![])
7400 }
7401 }
7402 ShapeBoundary::Polygon { points } => {
7403 let segments = polygon_line_intersection(points, y, line_height)?;
7404 Ok(segments
7405 .iter()
7406 .map(|s| (s.start_x, s.start_x + s.width))
7407 .collect())
7408 }
7409 ShapeBoundary::Path { .. } => Ok(vec![]), }
7411}
7412
7413fn merge_segments(mut segments: Vec<LineSegment>) -> Vec<LineSegment> {
7415 if segments.len() <= 1 {
7416 return segments;
7417 }
7418 segments.sort_by(|a, b| a.start_x.partial_cmp(&b.start_x).unwrap());
7419 let mut merged = vec![segments[0].clone()];
7420 for next_seg in segments.iter().skip(1) {
7421 let last = merged.last_mut().unwrap();
7422 if next_seg.start_x <= last.start_x + last.width {
7423 let new_width = (next_seg.start_x + next_seg.width) - last.start_x;
7424 last.width = last.width.max(new_width);
7425 } else {
7426 merged.push(next_seg.clone());
7427 }
7428 }
7429 merged
7430}
7431
7432fn polygon_line_intersection(
7434 points: &[Point],
7435 y: f32,
7436 line_height: f32,
7437) -> Result<Vec<LineSegment>, LayoutError> {
7438 if points.len() < 3 {
7439 return Ok(vec![]);
7440 }
7441
7442 let line_center_y = y + line_height / 2.0;
7443 let mut intersections = Vec::new();
7444
7445 for i in 0..points.len() {
7447 let p1 = points[i];
7448 let p2 = points[(i + 1) % points.len()];
7449
7450 if (p2.y - p1.y).abs() < f32::EPSILON {
7452 continue;
7453 }
7454
7455 let crosses = (p1.y <= line_center_y && p2.y > line_center_y)
7457 || (p1.y > line_center_y && p2.y <= line_center_y);
7458
7459 if crosses {
7460 let t = (line_center_y - p1.y) / (p2.y - p1.y);
7462 let x = p1.x + t * (p2.x - p1.x);
7463 intersections.push(x);
7464 }
7465 }
7466
7467 intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
7469
7470 let mut segments = Vec::new();
7472 for chunk in intersections.chunks_exact(2) {
7473 let start_x = chunk[0];
7474 let end_x = chunk[1];
7475 if end_x > start_x {
7476 segments.push(LineSegment {
7477 start_x,
7478 width: end_x - start_x,
7479 priority: 0,
7480 });
7481 }
7482 }
7483
7484 Ok(segments)
7485}
7486
7487#[cfg(feature = "text_layout_hyphenation")]
7491fn get_hyphenator(language: HyphenationLanguage) -> Result<Standard, LayoutError> {
7492 Standard::from_embedded(language).map_err(|e| LayoutError::HyphenationError(e.to_string()))
7493}
7494
7495#[cfg(not(feature = "text_layout_hyphenation"))]
7497fn get_hyphenator(_language: Language) -> Result<Standard, LayoutError> {
7498 Err(LayoutError::HyphenationError("Hyphenation feature not enabled".to_string()))
7499}
7500
7501fn is_break_opportunity(item: &ShapedItem) -> bool {
7502 if is_word_separator(item) {
7504 return true;
7505 }
7506 if let ShapedItem::Break { .. } = item {
7507 return true;
7508 }
7509 if let ShapedItem::Cluster(c) = item {
7511 if c.text.starts_with('\u{00AD}') {
7512 return true;
7513 }
7514 }
7515 false
7516}
7517
7518pub struct BreakCursor<'a> {
7521 pub items: &'a [ShapedItem],
7523 pub next_item_index: usize,
7525 pub partial_remainder: Vec<ShapedItem>,
7528}
7529
7530impl<'a> BreakCursor<'a> {
7531 pub fn new(items: &'a [ShapedItem]) -> Self {
7532 Self {
7533 items,
7534 next_item_index: 0,
7535 partial_remainder: Vec::new(),
7536 }
7537 }
7538
7539 pub fn is_at_start(&self) -> bool {
7541 self.next_item_index == 0 && self.partial_remainder.is_empty()
7542 }
7543
7544 pub fn drain_remaining(&mut self) -> Vec<ShapedItem> {
7546 let mut remaining = std::mem::take(&mut self.partial_remainder);
7547 if self.next_item_index < self.items.len() {
7548 remaining.extend_from_slice(&self.items[self.next_item_index..]);
7549 }
7550 self.next_item_index = self.items.len();
7551 remaining
7552 }
7553
7554 pub fn is_done(&self) -> bool {
7556 self.next_item_index >= self.items.len() && self.partial_remainder.is_empty()
7557 }
7558
7559 pub fn consume(&mut self, count: usize) {
7561 if count == 0 {
7562 return;
7563 }
7564
7565 let remainder_len = self.partial_remainder.len();
7566 if count <= remainder_len {
7567 self.partial_remainder.drain(..count);
7569 } else {
7570 let from_main_list = count - remainder_len;
7572 self.partial_remainder.clear();
7573 self.next_item_index += from_main_list;
7574 }
7575 }
7576
7577 pub fn peek_next_unit(&self) -> Vec<ShapedItem> {
7581 let mut unit = Vec::new();
7582 let mut source_items = self.partial_remainder.clone();
7583 source_items.extend_from_slice(&self.items[self.next_item_index..]);
7584
7585 if source_items.is_empty() {
7586 return unit;
7587 }
7588
7589 if is_break_opportunity(&source_items[0]) {
7591 unit.push(source_items[0].clone());
7592 return unit;
7593 }
7594
7595 for item in source_items {
7597 if is_break_opportunity(&item) {
7598 break;
7599 }
7600 unit.push(item.clone());
7601 }
7602 unit
7603 }
7604}
7605
7606struct HyphenationResult {
7608 line_part: Vec<ShapedItem>,
7610 remainder_part: Vec<ShapedItem>,
7612}
7613
7614fn perform_bidi_analysis<'a, 'b: 'a>(
7615 styled_runs: &'a [TextRunInfo],
7616 full_text: &'b str,
7617 force_lang: Option<Language>,
7618) -> Result<(Vec<VisualRun<'a>>, BidiDirection), LayoutError> {
7619 if full_text.is_empty() {
7620 return Ok((Vec::new(), BidiDirection::Ltr));
7621 }
7622
7623 let bidi_info = BidiInfo::new(full_text, None);
7624 let para = &bidi_info.paragraphs[0];
7625 let base_direction = if para.level.is_rtl() {
7626 BidiDirection::Rtl
7627 } else {
7628 BidiDirection::Ltr
7629 };
7630
7631 let mut byte_to_run_index: Vec<usize> = vec![0; full_text.len()];
7633 for (run_idx, run) in styled_runs.iter().enumerate() {
7634 let start = run.logical_start;
7635 let end = start + run.text.len();
7636 for i in start..end {
7637 byte_to_run_index[i] = run_idx;
7638 }
7639 }
7640
7641 let mut final_visual_runs = Vec::new();
7642 let (levels, visual_run_ranges) = bidi_info.visual_runs(para, para.range.clone());
7643
7644 for range in visual_run_ranges {
7645 let bidi_level = levels[range.start];
7646 let mut sub_run_start = range.start;
7647
7648 for i in (range.start + 1)..range.end {
7650 if byte_to_run_index[i] != byte_to_run_index[sub_run_start] {
7651 let original_run_idx = byte_to_run_index[sub_run_start];
7653 let script = crate::text3::script::detect_script(&full_text[sub_run_start..i])
7654 .unwrap_or(Script::Latin);
7655 final_visual_runs.push(VisualRun {
7656 text_slice: &full_text[sub_run_start..i],
7657 style: styled_runs[original_run_idx].style.clone(),
7658 logical_start_byte: sub_run_start,
7659 bidi_level: BidiLevel::new(bidi_level.number()),
7660 language: force_lang.unwrap_or_else(|| {
7661 crate::text3::script::script_to_language(
7662 script,
7663 &full_text[sub_run_start..i],
7664 )
7665 }),
7666 script,
7667 });
7668 sub_run_start = i;
7670 }
7671 }
7672
7673 let original_run_idx = byte_to_run_index[sub_run_start];
7675 let script = crate::text3::script::detect_script(&full_text[sub_run_start..range.end])
7676 .unwrap_or(Script::Latin);
7677
7678 final_visual_runs.push(VisualRun {
7679 text_slice: &full_text[sub_run_start..range.end],
7680 style: styled_runs[original_run_idx].style.clone(),
7681 logical_start_byte: sub_run_start,
7682 bidi_level: BidiLevel::new(bidi_level.number()),
7683 script,
7684 language: force_lang.unwrap_or_else(|| {
7685 crate::text3::script::script_to_language(
7686 script,
7687 &full_text[sub_run_start..range.end],
7688 )
7689 }),
7690 });
7691 }
7692
7693 Ok((final_visual_runs, base_direction))
7694}
7695
7696fn get_justification_priority(class: CharacterClass) -> u8 {
7697 match class {
7698 CharacterClass::Space => 0,
7699 CharacterClass::Punctuation => 64,
7700 CharacterClass::Ideograph => 128,
7701 CharacterClass::Letter => 192,
7702 CharacterClass::Symbol => 224,
7703 CharacterClass::Combining => 255,
7704 }
7705}