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 {
99 match self {
100 AvailableSpace::Definite(v) => v,
101 AvailableSpace::MinContent => 0.0,
102 AvailableSpace::MaxContent => f32::MAX,
103 }
104 }
105
106 pub fn from_f32(value: f32) -> Self {
116 if value.is_infinite() || value >= f32::MAX / 2.0 {
117 AvailableSpace::MaxContent
119 } else if value <= 0.0 {
120 AvailableSpace::MinContent
122 } else {
123 AvailableSpace::Definite(value)
124 }
125 }
126}
127
128impl Hash for AvailableSpace {
129 fn hash<H: Hasher>(&self, state: &mut H) {
130 std::mem::discriminant(self).hash(state);
131 if let AvailableSpace::Definite(v) = self {
132 (v.round() as usize).hash(state);
133 }
134 }
135}
136
137pub use crate::font_traits::{ParsedFontTrait, ShallowClone};
139
140#[derive(Debug, Clone, PartialEq, Eq, Hash)]
144pub struct FontChainKey {
145 pub font_families: Vec<String>,
146 pub weight: FcWeight,
147 pub italic: bool,
148 pub oblique: bool,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Hash)]
157pub enum FontChainKeyOrRef {
158 Chain(FontChainKey),
160 Ref(usize),
162}
163
164impl FontChainKeyOrRef {
165 pub fn from_font_stack(font_stack: &FontStack) -> Self {
167 match font_stack {
168 FontStack::Stack(selectors) => FontChainKeyOrRef::Chain(FontChainKey::from_selectors(selectors)),
169 FontStack::Ref(font_ref) => FontChainKeyOrRef::Ref(font_ref.parsed as usize),
170 }
171 }
172
173 pub fn is_ref(&self) -> bool {
175 matches!(self, FontChainKeyOrRef::Ref(_))
176 }
177
178 pub fn as_ref_ptr(&self) -> Option<usize> {
180 match self {
181 FontChainKeyOrRef::Ref(ptr) => Some(*ptr),
182 _ => None,
183 }
184 }
185
186 pub fn as_chain(&self) -> Option<&FontChainKey> {
188 match self {
189 FontChainKeyOrRef::Chain(key) => Some(key),
190 _ => None,
191 }
192 }
193}
194
195impl FontChainKey {
196 pub fn from_selectors(font_stack: &[FontSelector]) -> Self {
198 let font_families: Vec<String> = font_stack
199 .iter()
200 .map(|s| s.family.clone())
201 .filter(|f| !f.is_empty())
202 .collect();
203
204 let font_families = if font_families.is_empty() {
205 vec!["serif".to_string()]
206 } else {
207 font_families
208 };
209
210 let weight = font_stack
211 .first()
212 .map(|s| s.weight)
213 .unwrap_or(FcWeight::Normal);
214 let is_italic = font_stack
215 .first()
216 .map(|s| s.style == FontStyle::Italic)
217 .unwrap_or(false);
218 let is_oblique = font_stack
219 .first()
220 .map(|s| s.style == FontStyle::Oblique)
221 .unwrap_or(false);
222
223 FontChainKey {
224 font_families,
225 weight,
226 italic: is_italic,
227 oblique: is_oblique,
228 }
229 }
230}
231
232#[derive(Debug, Clone)]
239pub struct LoadedFonts<T> {
240 pub fonts: HashMap<FontId, T>,
242 hash_to_id: HashMap<u64, FontId>,
244}
245
246impl<T: ParsedFontTrait> LoadedFonts<T> {
247 pub fn new() -> Self {
248 Self {
249 fonts: HashMap::new(),
250 hash_to_id: HashMap::new(),
251 }
252 }
253
254 pub fn insert(&mut self, font_id: FontId, font: T) {
256 let hash = font.get_hash();
257 self.hash_to_id.insert(hash, font_id.clone());
258 self.fonts.insert(font_id, font);
259 }
260
261 pub fn get(&self, font_id: &FontId) -> Option<&T> {
263 self.fonts.get(font_id)
264 }
265
266 pub fn get_by_hash(&self, hash: u64) -> Option<&T> {
268 self.hash_to_id.get(&hash).and_then(|id| self.fonts.get(id))
269 }
270
271 pub fn get_font_id_by_hash(&self, hash: u64) -> Option<&FontId> {
273 self.hash_to_id.get(&hash)
274 }
275
276 pub fn contains_key(&self, font_id: &FontId) -> bool {
278 self.fonts.contains_key(font_id)
279 }
280
281 pub fn contains_hash(&self, hash: u64) -> bool {
283 self.hash_to_id.contains_key(&hash)
284 }
285
286 pub fn iter(&self) -> impl Iterator<Item = (&FontId, &T)> {
288 self.fonts.iter()
289 }
290
291 pub fn len(&self) -> usize {
293 self.fonts.len()
294 }
295
296 pub fn is_empty(&self) -> bool {
298 self.fonts.is_empty()
299 }
300}
301
302impl<T: ParsedFontTrait> Default for LoadedFonts<T> {
303 fn default() -> Self {
304 Self::new()
305 }
306}
307
308impl<T: ParsedFontTrait> FromIterator<(FontId, T)> for LoadedFonts<T> {
309 fn from_iter<I: IntoIterator<Item = (FontId, T)>>(iter: I) -> Self {
310 let mut loaded = LoadedFonts::new();
311 for (id, font) in iter {
312 loaded.insert(id, font);
313 }
314 loaded
315 }
316}
317
318#[derive(Debug, Clone)]
323pub enum FontOrRef<T> {
324 Font(T),
326 Ref(azul_css::props::basic::FontRef),
328}
329
330impl<T: ParsedFontTrait> ShallowClone for FontOrRef<T> {
331 fn shallow_clone(&self) -> Self {
332 match self {
333 FontOrRef::Font(f) => FontOrRef::Font(f.shallow_clone()),
334 FontOrRef::Ref(r) => FontOrRef::Ref(r.clone()),
335 }
336 }
337}
338
339impl<T: ParsedFontTrait> ParsedFontTrait for FontOrRef<T> {
340 fn shape_text(
341 &self,
342 text: &str,
343 script: Script,
344 language: Language,
345 direction: BidiDirection,
346 style: &StyleProperties,
347 ) -> Result<Vec<Glyph>, LayoutError> {
348 match self {
349 FontOrRef::Font(f) => f.shape_text(text, script, language, direction, style),
350 FontOrRef::Ref(r) => r.shape_text(text, script, language, direction, style),
351 }
352 }
353
354 fn get_hash(&self) -> u64 {
355 match self {
356 FontOrRef::Font(f) => f.get_hash(),
357 FontOrRef::Ref(r) => r.get_hash(),
358 }
359 }
360
361 fn get_glyph_size(&self, glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
362 match self {
363 FontOrRef::Font(f) => f.get_glyph_size(glyph_id, font_size),
364 FontOrRef::Ref(r) => r.get_glyph_size(glyph_id, font_size),
365 }
366 }
367
368 fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
369 match self {
370 FontOrRef::Font(f) => f.get_hyphen_glyph_and_advance(font_size),
371 FontOrRef::Ref(r) => r.get_hyphen_glyph_and_advance(font_size),
372 }
373 }
374
375 fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
376 match self {
377 FontOrRef::Font(f) => f.get_kashida_glyph_and_advance(font_size),
378 FontOrRef::Ref(r) => r.get_kashida_glyph_and_advance(font_size),
379 }
380 }
381
382 fn has_glyph(&self, codepoint: u32) -> bool {
383 match self {
384 FontOrRef::Font(f) => f.has_glyph(codepoint),
385 FontOrRef::Ref(r) => r.has_glyph(codepoint),
386 }
387 }
388
389 fn get_vertical_metrics(&self, glyph_id: u16) -> Option<VerticalMetrics> {
390 match self {
391 FontOrRef::Font(f) => f.get_vertical_metrics(glyph_id),
392 FontOrRef::Ref(r) => r.get_vertical_metrics(glyph_id),
393 }
394 }
395
396 fn get_font_metrics(&self) -> LayoutFontMetrics {
397 match self {
398 FontOrRef::Font(f) => f.get_font_metrics(),
399 FontOrRef::Ref(r) => r.get_font_metrics(),
400 }
401 }
402
403 fn num_glyphs(&self) -> u16 {
404 match self {
405 FontOrRef::Font(f) => f.num_glyphs(),
406 FontOrRef::Ref(r) => r.num_glyphs(),
407 }
408 }
409}
410
411#[derive(Debug)]
412pub struct FontManager<T> {
413 pub fc_cache: Arc<FcFontCache>,
415 pub parsed_fonts: Mutex<HashMap<FontId, T>>,
417 pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
420 pub embedded_fonts: Mutex<HashMap<u64, azul_css::props::basic::FontRef>>,
423}
424
425impl<T: ParsedFontTrait> FontManager<T> {
426 pub fn new(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
427 Ok(Self {
428 fc_cache: Arc::new(fc_cache),
429 parsed_fonts: Mutex::new(HashMap::new()),
430 font_chain_cache: HashMap::new(), embedded_fonts: Mutex::new(HashMap::new()),
432 })
433 }
434
435 pub fn set_font_chain_cache(
440 &mut self,
441 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
442 ) {
443 self.font_chain_cache = chains;
444 }
445
446 pub fn merge_font_chain_cache(
450 &mut self,
451 chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
452 ) {
453 self.font_chain_cache.extend(chains);
454 }
455
456 pub fn get_font_chain_cache(
458 &self,
459 ) -> &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain> {
460 &self.font_chain_cache
461 }
462
463 pub fn get_embedded_font_by_hash(&self, font_hash: u64) -> Option<azul_css::props::basic::FontRef> {
466 let embedded = self.embedded_fonts.lock().unwrap();
467 embedded.get(&font_hash).cloned()
468 }
469
470 pub fn get_font_by_hash(&self, font_hash: u64) -> Option<T> {
473 let parsed = self.parsed_fonts.lock().unwrap();
474 for (_, font) in parsed.iter() {
476 if font.get_hash() == font_hash {
477 return Some(font.clone());
478 }
479 }
480 None
481 }
482
483 pub fn register_embedded_font(&self, font_ref: &azul_css::props::basic::FontRef) {
486 let hash = font_ref.get_hash();
487 let mut embedded = self.embedded_fonts.lock().unwrap();
488 embedded.insert(hash, font_ref.clone());
489 }
490
491 pub fn get_loaded_fonts(&self) -> LoadedFonts<T> {
498 let parsed = self.parsed_fonts.lock().unwrap();
499 parsed
500 .iter()
501 .map(|(id, font)| (id.clone(), font.shallow_clone()))
502 .collect()
503 }
504
505 pub fn get_loaded_font_ids(&self) -> std::collections::HashSet<FontId> {
510 let parsed = self.parsed_fonts.lock().unwrap();
511 parsed.keys().cloned().collect()
512 }
513
514 pub fn insert_font(&self, font_id: FontId, font: T) -> Option<T> {
518 let mut parsed = self.parsed_fonts.lock().unwrap();
519 parsed.insert(font_id, font)
520 }
521
522 pub fn insert_fonts(&self, fonts: impl IntoIterator<Item = (FontId, T)>) {
527 let mut parsed = self.parsed_fonts.lock().unwrap();
528 for (font_id, font) in fonts {
529 parsed.insert(font_id, font);
530 }
531 }
532
533 pub fn remove_font(&self, font_id: &FontId) -> Option<T> {
537 let mut parsed = self.parsed_fonts.lock().unwrap();
538 parsed.remove(font_id)
539 }
540}
541
542#[derive(Debug, thiserror::Error)]
544pub enum LayoutError {
545 #[error("Bidi analysis failed: {0}")]
546 BidiError(String),
547 #[error("Shaping failed: {0}")]
548 ShapingError(String),
549 #[error("Font not found: {0:?}")]
550 FontNotFound(FontSelector),
551 #[error("Invalid text input: {0}")]
552 InvalidText(String),
553 #[error("Hyphenation failed: {0}")]
554 HyphenationError(String),
555}
556
557#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub enum TextBoundary {
560 Top,
562 Bottom,
564 Start,
566 End,
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572pub struct CursorBoundsError {
573 pub boundary: TextBoundary,
575 pub cursor: TextCursor,
577}
578
579#[derive(Debug, Clone)]
643pub struct UnifiedConstraints {
644 pub shape_boundaries: Vec<ShapeBoundary>,
646 pub shape_exclusions: Vec<ShapeBoundary>,
647
648 pub available_width: AvailableSpace,
650 pub available_height: Option<f32>,
651
652 pub writing_mode: Option<WritingMode>,
654 pub direction: Option<BidiDirection>,
656 pub text_orientation: TextOrientation,
657 pub text_align: TextAlign,
658 pub text_justify: JustifyContent,
659 pub line_height: f32,
660 pub vertical_align: VerticalAlign,
661
662 pub overflow: OverflowBehavior,
664 pub segment_alignment: SegmentAlignment,
665
666 pub text_combine_upright: Option<TextCombineUpright>,
668 pub exclusion_margin: f32,
669 pub hyphenation: bool,
670 pub hyphenation_language: Option<Language>,
671 pub text_indent: f32,
672 pub initial_letter: Option<InitialLetter>,
673 pub line_clamp: Option<NonZeroUsize>,
674
675 pub text_wrap: TextWrap,
677 pub columns: u32,
678 pub column_gap: f32,
679 pub hanging_punctuation: bool,
680}
681
682impl Default for UnifiedConstraints {
683 fn default() -> Self {
684 Self {
685 shape_boundaries: Vec::new(),
686 shape_exclusions: Vec::new(),
687
688 available_width: AvailableSpace::MaxContent,
695 available_height: None,
696 writing_mode: None,
697 direction: None, text_orientation: TextOrientation::default(),
699 text_align: TextAlign::default(),
700 text_justify: JustifyContent::default(),
701 line_height: 16.0, vertical_align: VerticalAlign::default(),
703 overflow: OverflowBehavior::default(),
704 segment_alignment: SegmentAlignment::default(),
705 text_combine_upright: None,
706 exclusion_margin: 0.0,
707 hyphenation: false,
708 hyphenation_language: None,
709 columns: 1,
710 column_gap: 0.0,
711 hanging_punctuation: false,
712 text_indent: 0.0,
713 initial_letter: None,
714 line_clamp: None,
715 text_wrap: TextWrap::default(),
716 }
717 }
718}
719
720impl Hash for UnifiedConstraints {
722 fn hash<H: Hasher>(&self, state: &mut H) {
723 self.shape_boundaries.hash(state);
724 self.shape_exclusions.hash(state);
725 self.available_width.hash(state);
726 self.available_height
727 .map(|h| h.round() as usize)
728 .hash(state);
729 self.writing_mode.hash(state);
730 self.direction.hash(state);
731 self.text_orientation.hash(state);
732 self.text_align.hash(state);
733 self.text_justify.hash(state);
734 (self.line_height.round() as usize).hash(state);
735 self.vertical_align.hash(state);
736 self.overflow.hash(state);
737 self.text_combine_upright.hash(state);
738 (self.exclusion_margin.round() as usize).hash(state);
739 self.hyphenation.hash(state);
740 self.hyphenation_language.hash(state);
741 self.columns.hash(state);
742 (self.column_gap.round() as usize).hash(state);
743 self.hanging_punctuation.hash(state);
744 }
745}
746
747impl PartialEq for UnifiedConstraints {
748 fn eq(&self, other: &Self) -> bool {
749 self.shape_boundaries == other.shape_boundaries
750 && self.shape_exclusions == other.shape_exclusions
751 && self.available_width == other.available_width
752 && match (self.available_height, other.available_height) {
753 (None, None) => true,
754 (Some(h1), Some(h2)) => round_eq(h1, h2),
755 _ => false,
756 }
757 && self.writing_mode == other.writing_mode
758 && self.direction == other.direction
759 && self.text_orientation == other.text_orientation
760 && self.text_align == other.text_align
761 && self.text_justify == other.text_justify
762 && round_eq(self.line_height, other.line_height)
763 && self.vertical_align == other.vertical_align
764 && self.overflow == other.overflow
765 && self.text_combine_upright == other.text_combine_upright
766 && round_eq(self.exclusion_margin, other.exclusion_margin)
767 && self.hyphenation == other.hyphenation
768 && self.hyphenation_language == other.hyphenation_language
769 && self.columns == other.columns
770 && round_eq(self.column_gap, other.column_gap)
771 && self.hanging_punctuation == other.hanging_punctuation
772 }
773}
774
775impl Eq for UnifiedConstraints {}
776
777impl UnifiedConstraints {
778 fn direction(&self, fallback: BidiDirection) -> BidiDirection {
779 match self.writing_mode {
780 Some(s) => s.get_direction().unwrap_or(fallback),
781 None => fallback,
782 }
783 }
784 fn is_vertical(&self) -> bool {
785 matches!(
786 self.writing_mode,
787 Some(WritingMode::VerticalRl) | Some(WritingMode::VerticalLr)
788 )
789 }
790}
791
792#[derive(Debug, Clone)]
794pub struct LineConstraints {
795 pub segments: Vec<LineSegment>,
796 pub total_available: f32,
797}
798
799impl WritingMode {
800 fn get_direction(&self) -> Option<BidiDirection> {
801 match self {
802 WritingMode::HorizontalTb => None,
804 WritingMode::VerticalRl => Some(BidiDirection::Rtl),
805 WritingMode::VerticalLr => Some(BidiDirection::Ltr),
806 WritingMode::SidewaysRl => Some(BidiDirection::Rtl),
807 WritingMode::SidewaysLr => Some(BidiDirection::Ltr),
808 }
809 }
810}
811
812#[derive(Debug, Clone, Hash)]
814pub struct StyledRun {
815 pub text: String,
816 pub style: Arc<StyleProperties>,
817 pub logical_start_byte: usize,
819 pub source_node_id: Option<NodeId>,
822}
823
824#[derive(Debug, Clone)]
826pub struct VisualRun<'a> {
827 pub text_slice: &'a str,
828 pub style: Arc<StyleProperties>,
829 pub logical_start_byte: usize,
830 pub bidi_level: BidiLevel,
831 pub script: Script,
832 pub language: Language,
833}
834
835#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
840pub struct FontSelector {
841 pub family: String,
842 pub weight: FcWeight,
843 pub style: FontStyle,
844 pub unicode_ranges: Vec<UnicodeRange>,
845}
846
847impl Default for FontSelector {
848 fn default() -> Self {
849 Self {
850 family: "serif".to_string(),
851 weight: FcWeight::Normal,
852 style: FontStyle::Normal,
853 unicode_ranges: Vec::new(),
854 }
855 }
856}
857
858#[derive(Debug, Clone)]
865pub enum FontStack {
866 Stack(Vec<FontSelector>),
869 Ref(azul_css::props::basic::font::FontRef),
872}
873
874impl Default for FontStack {
875 fn default() -> Self {
876 FontStack::Stack(vec![FontSelector::default()])
877 }
878}
879
880impl FontStack {
881 pub fn is_ref(&self) -> bool {
883 matches!(self, FontStack::Ref(_))
884 }
885
886 pub fn as_ref(&self) -> Option<&azul_css::props::basic::font::FontRef> {
888 match self {
889 FontStack::Ref(r) => Some(r),
890 _ => None,
891 }
892 }
893
894 pub fn as_stack(&self) -> Option<&[FontSelector]> {
896 match self {
897 FontStack::Stack(s) => Some(s),
898 _ => None,
899 }
900 }
901
902 pub fn first_selector(&self) -> Option<&FontSelector> {
904 match self {
905 FontStack::Stack(s) => s.first(),
906 FontStack::Ref(_) => None,
907 }
908 }
909
910 pub fn first_family(&self) -> &str {
912 match self {
913 FontStack::Stack(s) => s.first().map(|f| f.family.as_str()).unwrap_or("serif"),
914 FontStack::Ref(_) => "<embedded-font>",
915 }
916 }
917}
918
919impl PartialEq for FontStack {
920 fn eq(&self, other: &Self) -> bool {
921 match (self, other) {
922 (FontStack::Stack(a), FontStack::Stack(b)) => a == b,
923 (FontStack::Ref(a), FontStack::Ref(b)) => a.parsed == b.parsed,
924 _ => false,
925 }
926 }
927}
928
929impl Eq for FontStack {}
930
931impl Hash for FontStack {
932 fn hash<H: Hasher>(&self, state: &mut H) {
933 core::mem::discriminant(self).hash(state);
934 match self {
935 FontStack::Stack(s) => s.hash(state),
936 FontStack::Ref(r) => (r.parsed as usize).hash(state),
937 }
938 }
939}
940
941#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
945pub struct FontHash {
946 pub font_hash: u64,
948}
949
950impl FontHash {
951 pub fn invalid() -> Self {
952 Self { font_hash: 0 }
953 }
954
955 pub fn from_hash(font_hash: u64) -> Self {
956 Self { font_hash }
957 }
958}
959
960#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
961pub enum FontStyle {
962 Normal,
963 Italic,
964 Oblique,
965}
966
967#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
969pub enum SegmentAlignment {
970 #[default]
972 First,
973 Total,
976}
977
978#[derive(Debug, Clone)]
979pub struct VerticalMetrics {
980 pub advance: f32,
981 pub bearing_x: f32,
982 pub bearing_y: f32,
983 pub origin_y: f32,
984}
985
986#[derive(Debug, Clone)]
989pub struct LayoutFontMetrics {
990 pub ascent: f32,
991 pub descent: f32,
992 pub line_gap: f32,
993 pub units_per_em: u16,
994}
995
996impl LayoutFontMetrics {
997 pub fn baseline_scaled(&self, font_size: f32) -> f32 {
998 let scale = font_size / self.units_per_em as f32;
999 self.ascent * scale
1000 }
1001
1002 pub fn from_font_metrics(metrics: &azul_css::props::basic::FontMetrics) -> Self {
1004 Self {
1005 ascent: metrics.ascender as f32,
1006 descent: metrics.descender as f32,
1007 line_gap: metrics.line_gap as f32,
1008 units_per_em: metrics.units_per_em,
1009 }
1010 }
1011}
1012
1013#[derive(Debug, Clone)]
1014pub struct LineSegment {
1015 pub start_x: f32,
1016 pub width: f32,
1017 pub priority: u8,
1019}
1020
1021#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
1022pub enum TextWrap {
1023 #[default]
1024 Wrap,
1025 Balance,
1026 NoWrap,
1027}
1028
1029#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
1031pub struct InitialLetter {
1032 pub size: f32,
1034 pub sink: u32,
1036 pub count: NonZeroUsize,
1038}
1039
1040impl Eq for InitialLetter {}
1045
1046impl Hash for InitialLetter {
1047 fn hash<H: Hasher>(&self, state: &mut H) {
1048 (self.size.round() as usize).hash(state);
1053 self.sink.hash(state);
1054 self.count.hash(state);
1055 }
1056}
1057
1058#[derive(Debug, Clone, PartialOrd)]
1060pub enum PathSegment {
1061 MoveTo(Point),
1062 LineTo(Point),
1063 CurveTo {
1064 control1: Point,
1065 control2: Point,
1066 end: Point,
1067 },
1068 QuadTo {
1069 control: Point,
1070 end: Point,
1071 },
1072 Arc {
1073 center: Point,
1074 radius: f32,
1075 start_angle: f32,
1076 end_angle: f32,
1077 },
1078 Close,
1079}
1080
1081impl Hash for PathSegment {
1083 fn hash<H: Hasher>(&self, state: &mut H) {
1084 discriminant(self).hash(state);
1086
1087 match self {
1088 PathSegment::MoveTo(p) => p.hash(state),
1089 PathSegment::LineTo(p) => p.hash(state),
1090 PathSegment::CurveTo {
1091 control1,
1092 control2,
1093 end,
1094 } => {
1095 control1.hash(state);
1096 control2.hash(state);
1097 end.hash(state);
1098 }
1099 PathSegment::QuadTo { control, end } => {
1100 control.hash(state);
1101 end.hash(state);
1102 }
1103 PathSegment::Arc {
1104 center,
1105 radius,
1106 start_angle,
1107 end_angle,
1108 } => {
1109 center.hash(state);
1110 (radius.round() as usize).hash(state);
1111 (start_angle.round() as usize).hash(state);
1112 (end_angle.round() as usize).hash(state);
1113 }
1114 PathSegment::Close => {} }
1116 }
1117}
1118
1119impl PartialEq for PathSegment {
1120 fn eq(&self, other: &Self) -> bool {
1121 match (self, other) {
1122 (PathSegment::MoveTo(a), PathSegment::MoveTo(b)) => a == b,
1123 (PathSegment::LineTo(a), PathSegment::LineTo(b)) => a == b,
1124 (
1125 PathSegment::CurveTo {
1126 control1: c1a,
1127 control2: c2a,
1128 end: ea,
1129 },
1130 PathSegment::CurveTo {
1131 control1: c1b,
1132 control2: c2b,
1133 end: eb,
1134 },
1135 ) => c1a == c1b && c2a == c2b && ea == eb,
1136 (
1137 PathSegment::QuadTo {
1138 control: ca,
1139 end: ea,
1140 },
1141 PathSegment::QuadTo {
1142 control: cb,
1143 end: eb,
1144 },
1145 ) => ca == cb && ea == eb,
1146 (
1147 PathSegment::Arc {
1148 center: ca,
1149 radius: ra,
1150 start_angle: sa_a,
1151 end_angle: ea_a,
1152 },
1153 PathSegment::Arc {
1154 center: cb,
1155 radius: rb,
1156 start_angle: sa_b,
1157 end_angle: ea_b,
1158 },
1159 ) => ca == cb && round_eq(*ra, *rb) && round_eq(*sa_a, *sa_b) && round_eq(*ea_a, *ea_b),
1160 (PathSegment::Close, PathSegment::Close) => true,
1161 _ => false, }
1163 }
1164}
1165
1166impl Eq for PathSegment {}
1167
1168#[derive(Debug, Clone, Hash)]
1170pub enum InlineContent {
1171 Text(StyledRun),
1172 Image(InlineImage),
1173 Shape(InlineShape),
1174 Space(InlineSpace),
1175 LineBreak(InlineBreak),
1176 Tab,
1177 Marker {
1181 run: StyledRun,
1182 position_outside: bool,
1184 },
1185 Ruby {
1187 base: Vec<InlineContent>,
1188 text: Vec<InlineContent>,
1189 style: Arc<StyleProperties>,
1191 },
1192}
1193
1194#[derive(Debug, Clone)]
1195pub struct InlineImage {
1196 pub source: ImageSource,
1197 pub intrinsic_size: Size,
1198 pub display_size: Option<Size>,
1199 pub baseline_offset: f32,
1201 pub alignment: VerticalAlign,
1202 pub object_fit: ObjectFit,
1203}
1204
1205impl PartialEq for InlineImage {
1206 fn eq(&self, other: &Self) -> bool {
1207 self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
1208 && self.source == other.source
1209 && self.intrinsic_size == other.intrinsic_size
1210 && self.display_size == other.display_size
1211 && self.alignment == other.alignment
1212 && self.object_fit == other.object_fit
1213 }
1214}
1215
1216impl Eq for InlineImage {}
1217
1218impl Hash for InlineImage {
1219 fn hash<H: Hasher>(&self, state: &mut H) {
1220 self.source.hash(state);
1221 self.intrinsic_size.hash(state);
1222 self.display_size.hash(state);
1223 self.baseline_offset.to_bits().hash(state);
1224 self.alignment.hash(state);
1225 self.object_fit.hash(state);
1226 }
1227}
1228
1229impl PartialOrd for InlineImage {
1230 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1231 Some(self.cmp(other))
1232 }
1233}
1234
1235impl Ord for InlineImage {
1236 fn cmp(&self, other: &Self) -> Ordering {
1237 self.source
1238 .cmp(&other.source)
1239 .then_with(|| self.intrinsic_size.cmp(&other.intrinsic_size))
1240 .then_with(|| self.display_size.cmp(&other.display_size))
1241 .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
1242 .then_with(|| self.alignment.cmp(&other.alignment))
1243 .then_with(|| self.object_fit.cmp(&other.object_fit))
1244 }
1245}
1246
1247#[derive(Debug, Clone)]
1249pub struct Glyph {
1250 pub glyph_id: u16,
1252 pub codepoint: char,
1253 pub font_hash: u64,
1255 pub font_metrics: LayoutFontMetrics,
1257 pub style: Arc<StyleProperties>,
1258 pub source: GlyphSource,
1259
1260 pub logical_byte_index: usize,
1262 pub logical_byte_len: usize,
1263 pub content_index: usize,
1264 pub cluster: u32,
1265
1266 pub advance: f32,
1268 pub kerning: f32,
1269 pub offset: Point,
1270
1271 pub vertical_advance: f32,
1273 pub vertical_origin_y: f32, pub vertical_bearing: Point,
1275 pub orientation: GlyphOrientation,
1276
1277 pub script: Script,
1279 pub bidi_level: BidiLevel,
1280}
1281
1282impl Glyph {
1283 #[inline]
1284 fn bounds(&self) -> Rect {
1285 Rect {
1286 x: 0.0,
1287 y: 0.0,
1288 width: self.advance,
1289 height: self.style.line_height,
1290 }
1291 }
1292
1293 #[inline]
1294 fn character_class(&self) -> CharacterClass {
1295 classify_character(self.codepoint as u32)
1296 }
1297
1298 #[inline]
1299 fn is_whitespace(&self) -> bool {
1300 self.character_class() == CharacterClass::Space
1301 }
1302
1303 #[inline]
1304 fn can_justify(&self) -> bool {
1305 !self.codepoint.is_whitespace() && self.character_class() != CharacterClass::Combining
1306 }
1307
1308 #[inline]
1309 fn justification_priority(&self) -> u8 {
1310 get_justification_priority(self.character_class())
1311 }
1312
1313 #[inline]
1314 fn break_opportunity_after(&self) -> bool {
1315 let is_whitespace = self.codepoint.is_whitespace();
1316 let is_soft_hyphen = self.codepoint == '\u{00AD}';
1317 is_whitespace || is_soft_hyphen
1318 }
1319}
1320
1321#[derive(Debug, Clone)]
1323pub struct TextRunInfo<'a> {
1324 pub text: &'a str,
1325 pub style: Arc<StyleProperties>,
1326 pub logical_start: usize,
1327 pub content_index: usize,
1328}
1329
1330#[derive(Debug, Clone)]
1331pub enum ImageSource {
1332 Ref(ImageRef),
1334 Url(String),
1336 Data(Arc<[u8]>),
1338 Svg(Arc<str>),
1340 Placeholder(Size),
1342}
1343
1344impl PartialEq for ImageSource {
1345 fn eq(&self, other: &Self) -> bool {
1346 match (self, other) {
1347 (ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash() == b.get_hash(),
1348 (ImageSource::Url(a), ImageSource::Url(b)) => a == b,
1349 (ImageSource::Data(a), ImageSource::Data(b)) => Arc::ptr_eq(a, b),
1350 (ImageSource::Svg(a), ImageSource::Svg(b)) => Arc::ptr_eq(a, b),
1351 (ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
1352 a.width.to_bits() == b.width.to_bits() && a.height.to_bits() == b.height.to_bits()
1353 }
1354 _ => false,
1355 }
1356 }
1357}
1358
1359impl Eq for ImageSource {}
1360
1361impl std::hash::Hash for ImageSource {
1362 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1363 core::mem::discriminant(self).hash(state);
1364 match self {
1365 ImageSource::Ref(r) => r.get_hash().hash(state),
1366 ImageSource::Url(s) => s.hash(state),
1367 ImageSource::Data(d) => (Arc::as_ptr(d) as *const u8 as usize).hash(state),
1368 ImageSource::Svg(s) => (Arc::as_ptr(s) as *const u8 as usize).hash(state),
1369 ImageSource::Placeholder(sz) => {
1370 sz.width.to_bits().hash(state);
1371 sz.height.to_bits().hash(state);
1372 }
1373 }
1374 }
1375}
1376
1377impl PartialOrd for ImageSource {
1378 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1379 Some(self.cmp(other))
1380 }
1381}
1382
1383impl Ord for ImageSource {
1384 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1385 fn variant_index(s: &ImageSource) -> u8 {
1386 match s {
1387 ImageSource::Ref(_) => 0,
1388 ImageSource::Url(_) => 1,
1389 ImageSource::Data(_) => 2,
1390 ImageSource::Svg(_) => 3,
1391 ImageSource::Placeholder(_) => 4,
1392 }
1393 }
1394 match (self, other) {
1395 (ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash().cmp(&b.get_hash()),
1396 (ImageSource::Url(a), ImageSource::Url(b)) => a.cmp(b),
1397 (ImageSource::Data(a), ImageSource::Data(b)) => {
1398 (Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
1399 }
1400 (ImageSource::Svg(a), ImageSource::Svg(b)) => {
1401 (Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
1402 }
1403 (ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
1404 (a.width.to_bits(), a.height.to_bits())
1405 .cmp(&(b.width.to_bits(), b.height.to_bits()))
1406 }
1407 _ => variant_index(self).cmp(&variant_index(other)),
1409 }
1410 }
1411}
1412
1413#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
1414pub enum VerticalAlign {
1415 #[default]
1417 Baseline,
1418 Bottom,
1420 Top,
1422 Middle,
1424 TextTop,
1426 TextBottom,
1428 Sub,
1430 Super,
1432 Offset(f32),
1434}
1435
1436impl std::hash::Hash for VerticalAlign {
1437 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1438 core::mem::discriminant(self).hash(state);
1439 if let VerticalAlign::Offset(f) = self {
1440 f.to_bits().hash(state);
1441 }
1442 }
1443}
1444
1445impl Eq for VerticalAlign {}
1446
1447impl Ord for VerticalAlign {
1448 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1449 self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
1450 }
1451}
1452
1453#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
1454pub enum ObjectFit {
1455 Fill,
1457 Contain,
1459 Cover,
1461 None,
1463 ScaleDown,
1465}
1466
1467#[derive(Debug, Clone, PartialEq)]
1473pub struct InlineBorderInfo {
1474 pub top: f32,
1476 pub right: f32,
1477 pub bottom: f32,
1478 pub left: f32,
1479 pub top_color: ColorU,
1481 pub right_color: ColorU,
1482 pub bottom_color: ColorU,
1483 pub left_color: ColorU,
1484 pub radius: Option<f32>,
1486}
1487
1488impl Default for InlineBorderInfo {
1489 fn default() -> Self {
1490 Self {
1491 top: 0.0,
1492 right: 0.0,
1493 bottom: 0.0,
1494 left: 0.0,
1495 top_color: ColorU::TRANSPARENT,
1496 right_color: ColorU::TRANSPARENT,
1497 bottom_color: ColorU::TRANSPARENT,
1498 left_color: ColorU::TRANSPARENT,
1499 radius: None,
1500 }
1501 }
1502}
1503
1504impl InlineBorderInfo {
1505 pub fn has_border(&self) -> bool {
1507 self.top > 0.0 || self.right > 0.0 || self.bottom > 0.0 || self.left > 0.0
1508 }
1509}
1510
1511#[derive(Debug, Clone)]
1512pub struct InlineShape {
1513 pub shape_def: ShapeDefinition,
1514 pub fill: Option<ColorU>,
1515 pub stroke: Option<Stroke>,
1516 pub baseline_offset: f32,
1517 pub source_node_id: Option<azul_core::dom::NodeId>,
1521}
1522
1523#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1524pub enum OverflowBehavior {
1525 Visible,
1527 Hidden,
1529 Scroll,
1531 #[default]
1533 Auto,
1534 Break,
1536}
1537
1538#[derive(Debug, Clone)]
1539pub struct MeasuredImage {
1540 pub source: ImageSource,
1541 pub size: Size,
1542 pub baseline_offset: f32,
1543 pub alignment: VerticalAlign,
1544 pub content_index: usize,
1545}
1546
1547#[derive(Debug, Clone)]
1548pub struct MeasuredShape {
1549 pub shape_def: ShapeDefinition,
1550 pub size: Size,
1551 pub baseline_offset: f32,
1552 pub content_index: usize,
1553}
1554
1555#[derive(Debug, Clone)]
1556pub struct InlineSpace {
1557 pub width: f32,
1558 pub is_breaking: bool, pub is_stretchy: bool, }
1561
1562impl PartialEq for InlineSpace {
1563 fn eq(&self, other: &Self) -> bool {
1564 self.width.to_bits() == other.width.to_bits()
1565 && self.is_breaking == other.is_breaking
1566 && self.is_stretchy == other.is_stretchy
1567 }
1568}
1569
1570impl Eq for InlineSpace {}
1571
1572impl Hash for InlineSpace {
1573 fn hash<H: Hasher>(&self, state: &mut H) {
1574 self.width.to_bits().hash(state);
1575 self.is_breaking.hash(state);
1576 self.is_stretchy.hash(state);
1577 }
1578}
1579
1580impl PartialOrd for InlineSpace {
1581 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1582 Some(self.cmp(other))
1583 }
1584}
1585
1586impl Ord for InlineSpace {
1587 fn cmp(&self, other: &Self) -> Ordering {
1588 self.width
1589 .total_cmp(&other.width)
1590 .then_with(|| self.is_breaking.cmp(&other.is_breaking))
1591 .then_with(|| self.is_stretchy.cmp(&other.is_stretchy))
1592 }
1593}
1594
1595impl PartialEq for InlineShape {
1596 fn eq(&self, other: &Self) -> bool {
1597 self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
1598 && self.shape_def == other.shape_def
1599 && self.fill == other.fill
1600 && self.stroke == other.stroke
1601 && self.source_node_id == other.source_node_id
1602 }
1603}
1604
1605impl Eq for InlineShape {}
1606
1607impl Hash for InlineShape {
1608 fn hash<H: Hasher>(&self, state: &mut H) {
1609 self.shape_def.hash(state);
1610 self.fill.hash(state);
1611 self.stroke.hash(state);
1612 self.baseline_offset.to_bits().hash(state);
1613 self.source_node_id.hash(state);
1614 }
1615}
1616
1617impl PartialOrd for InlineShape {
1618 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1619 Some(
1620 self.shape_def
1621 .partial_cmp(&other.shape_def)?
1622 .then_with(|| self.fill.cmp(&other.fill))
1623 .then_with(|| {
1624 self.stroke
1625 .partial_cmp(&other.stroke)
1626 .unwrap_or(Ordering::Equal)
1627 })
1628 .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
1629 .then_with(|| self.source_node_id.cmp(&other.source_node_id)),
1630 )
1631 }
1632}
1633
1634#[derive(Debug, Default, Clone, Copy)]
1635pub struct Rect {
1636 pub x: f32,
1637 pub y: f32,
1638 pub width: f32,
1639 pub height: f32,
1640}
1641
1642impl PartialEq for Rect {
1643 fn eq(&self, other: &Self) -> bool {
1644 round_eq(self.x, other.x)
1645 && round_eq(self.y, other.y)
1646 && round_eq(self.width, other.width)
1647 && round_eq(self.height, other.height)
1648 }
1649}
1650impl Eq for Rect {}
1651
1652impl Hash for Rect {
1653 fn hash<H: Hasher>(&self, state: &mut H) {
1654 (self.x.round() as usize).hash(state);
1657 (self.y.round() as usize).hash(state);
1658 (self.width.round() as usize).hash(state);
1659 (self.height.round() as usize).hash(state);
1660 }
1661}
1662
1663#[derive(Debug, Default, Clone, Copy, PartialOrd)]
1664pub struct Size {
1665 pub width: f32,
1666 pub height: f32,
1667}
1668
1669impl Ord for Size {
1670 fn cmp(&self, other: &Self) -> Ordering {
1671 (self.width.round() as usize)
1672 .cmp(&(other.width.round() as usize))
1673 .then_with(|| (self.height.round() as usize).cmp(&(other.height.round() as usize)))
1674 }
1675}
1676
1677impl Hash for Size {
1679 fn hash<H: Hasher>(&self, state: &mut H) {
1680 (self.width.round() as usize).hash(state);
1681 (self.height.round() as usize).hash(state);
1682 }
1683}
1684impl PartialEq for Size {
1685 fn eq(&self, other: &Self) -> bool {
1686 round_eq(self.width, other.width) && round_eq(self.height, other.height)
1687 }
1688}
1689impl Eq for Size {}
1690
1691impl Size {
1692 pub const fn zero() -> Self {
1693 Self::new(0.0, 0.0)
1694 }
1695 pub const fn new(width: f32, height: f32) -> Self {
1696 Self { width, height }
1697 }
1698}
1699
1700#[derive(Debug, Default, Clone, Copy, PartialOrd)]
1701pub struct Point {
1702 pub x: f32,
1703 pub y: f32,
1704}
1705
1706impl Hash for Point {
1708 fn hash<H: Hasher>(&self, state: &mut H) {
1709 (self.x.round() as usize).hash(state);
1710 (self.y.round() as usize).hash(state);
1711 }
1712}
1713
1714impl PartialEq for Point {
1715 fn eq(&self, other: &Self) -> bool {
1716 round_eq(self.x, other.x) && round_eq(self.y, other.y)
1717 }
1718}
1719
1720impl Eq for Point {}
1721
1722#[derive(Debug, Clone, PartialOrd)]
1723pub enum ShapeDefinition {
1724 Rectangle {
1725 size: Size,
1726 corner_radius: Option<f32>,
1727 },
1728 Circle {
1729 radius: f32,
1730 },
1731 Ellipse {
1732 radii: Size,
1733 },
1734 Polygon {
1735 points: Vec<Point>,
1736 },
1737 Path {
1738 segments: Vec<PathSegment>,
1739 },
1740}
1741
1742impl Hash for ShapeDefinition {
1744 fn hash<H: Hasher>(&self, state: &mut H) {
1745 discriminant(self).hash(state);
1746 match self {
1747 ShapeDefinition::Rectangle {
1748 size,
1749 corner_radius,
1750 } => {
1751 size.hash(state);
1752 corner_radius.map(|r| r.round() as usize).hash(state);
1753 }
1754 ShapeDefinition::Circle { radius } => {
1755 (radius.round() as usize).hash(state);
1756 }
1757 ShapeDefinition::Ellipse { radii } => {
1758 radii.hash(state);
1759 }
1760 ShapeDefinition::Polygon { points } => {
1761 points.hash(state);
1763 }
1764 ShapeDefinition::Path { segments } => {
1765 segments.hash(state);
1767 }
1768 }
1769 }
1770}
1771
1772impl PartialEq for ShapeDefinition {
1773 fn eq(&self, other: &Self) -> bool {
1774 match (self, other) {
1775 (
1776 ShapeDefinition::Rectangle {
1777 size: s1,
1778 corner_radius: r1,
1779 },
1780 ShapeDefinition::Rectangle {
1781 size: s2,
1782 corner_radius: r2,
1783 },
1784 ) => {
1785 s1 == s2
1786 && match (r1, r2) {
1787 (None, None) => true,
1788 (Some(v1), Some(v2)) => round_eq(*v1, *v2),
1789 _ => false,
1790 }
1791 }
1792 (ShapeDefinition::Circle { radius: r1 }, ShapeDefinition::Circle { radius: r2 }) => {
1793 round_eq(*r1, *r2)
1794 }
1795 (ShapeDefinition::Ellipse { radii: r1 }, ShapeDefinition::Ellipse { radii: r2 }) => {
1796 r1 == r2
1797 }
1798 (ShapeDefinition::Polygon { points: p1 }, ShapeDefinition::Polygon { points: p2 }) => {
1799 p1 == p2
1800 }
1801 (ShapeDefinition::Path { segments: s1 }, ShapeDefinition::Path { segments: s2 }) => {
1802 s1 == s2
1803 }
1804 _ => false,
1805 }
1806 }
1807}
1808impl Eq for ShapeDefinition {}
1809
1810impl ShapeDefinition {
1811 pub fn get_size(&self) -> Size {
1813 match self {
1814 ShapeDefinition::Rectangle { size, .. } => *size,
1816
1817 ShapeDefinition::Circle { radius } => {
1819 let diameter = radius * 2.0;
1820 Size::new(diameter, diameter)
1821 }
1822
1823 ShapeDefinition::Ellipse { radii } => Size::new(radii.width * 2.0, radii.height * 2.0),
1825
1826 ShapeDefinition::Polygon { points } => calculate_bounding_box_size(points),
1828
1829 ShapeDefinition::Path { segments } => {
1836 let mut points = Vec::new();
1837 let mut current_pos = Point { x: 0.0, y: 0.0 };
1838
1839 for segment in segments {
1840 match segment {
1841 PathSegment::MoveTo(p) | PathSegment::LineTo(p) => {
1842 points.push(*p);
1843 current_pos = *p;
1844 }
1845 PathSegment::QuadTo { control, end } => {
1846 points.push(current_pos);
1847 points.push(*control);
1848 points.push(*end);
1849 current_pos = *end;
1850 }
1851 PathSegment::CurveTo {
1852 control1,
1853 control2,
1854 end,
1855 } => {
1856 points.push(current_pos);
1857 points.push(*control1);
1858 points.push(*control2);
1859 points.push(*end);
1860 current_pos = *end;
1861 }
1862 PathSegment::Arc {
1863 center,
1864 radius,
1865 start_angle,
1866 end_angle,
1867 } => {
1868 let start_point = Point {
1870 x: center.x + radius * start_angle.cos(),
1871 y: center.y + radius * start_angle.sin(),
1872 };
1873 let end_point = Point {
1874 x: center.x + radius * end_angle.cos(),
1875 y: center.y + radius * end_angle.sin(),
1876 };
1877 points.push(start_point);
1878 points.push(end_point);
1879
1880 let mut normalized_end = *end_angle;
1884 while normalized_end < *start_angle {
1885 normalized_end += 2.0 * std::f32::consts::PI;
1886 }
1887
1888 let mut check_angle = (*start_angle / std::f32::consts::FRAC_PI_2)
1891 .ceil()
1892 * std::f32::consts::FRAC_PI_2;
1893
1894 while check_angle < normalized_end {
1898 points.push(Point {
1899 x: center.x + radius * check_angle.cos(),
1900 y: center.y + radius * check_angle.sin(),
1901 });
1902 check_angle += std::f32::consts::FRAC_PI_2;
1903 }
1904
1905 current_pos = end_point;
1908 }
1909 PathSegment::Close => {
1910 }
1912 }
1913 }
1914 calculate_bounding_box_size(&points)
1915 }
1916 }
1917 }
1918}
1919
1920fn calculate_bounding_box_size(points: &[Point]) -> Size {
1922 if points.is_empty() {
1923 return Size::zero();
1924 }
1925
1926 let mut min_x = f32::MAX;
1927 let mut max_x = f32::MIN;
1928 let mut min_y = f32::MAX;
1929 let mut max_y = f32::MIN;
1930
1931 for point in points {
1932 min_x = min_x.min(point.x);
1933 max_x = max_x.max(point.x);
1934 min_y = min_y.min(point.y);
1935 max_y = max_y.max(point.y);
1936 }
1937
1938 if min_x > max_x || min_y > max_y {
1940 return Size::zero();
1941 }
1942
1943 Size::new(max_x - min_x, max_y - min_y)
1944}
1945
1946#[derive(Debug, Clone, PartialOrd)]
1947pub struct Stroke {
1948 pub color: ColorU,
1949 pub width: f32,
1950 pub dash_pattern: Option<Vec<f32>>,
1951}
1952
1953impl Hash for Stroke {
1955 fn hash<H: Hasher>(&self, state: &mut H) {
1956 self.color.hash(state);
1957 (self.width.round() as usize).hash(state);
1958
1959 match &self.dash_pattern {
1961 None => 0u8.hash(state), Some(pattern) => {
1963 1u8.hash(state); pattern.len().hash(state); for &val in pattern {
1966 (val.round() as usize).hash(state); }
1968 }
1969 }
1970 }
1971}
1972
1973impl PartialEq for Stroke {
1974 fn eq(&self, other: &Self) -> bool {
1975 if self.color != other.color || !round_eq(self.width, other.width) {
1976 return false;
1977 }
1978 match (&self.dash_pattern, &other.dash_pattern) {
1979 (None, None) => true,
1980 (Some(p1), Some(p2)) => {
1981 p1.len() == p2.len() && p1.iter().zip(p2.iter()).all(|(a, b)| round_eq(*a, *b))
1982 }
1983 _ => false,
1984 }
1985 }
1986}
1987
1988impl Eq for Stroke {}
1989
1990fn round_eq(a: f32, b: f32) -> bool {
1992 (a.round() as isize) == (b.round() as isize)
1993}
1994
1995#[derive(Debug, Clone)]
1996pub enum ShapeBoundary {
1997 Rectangle(Rect),
1998 Circle { center: Point, radius: f32 },
1999 Ellipse { center: Point, radii: Size },
2000 Polygon { points: Vec<Point> },
2001 Path { segments: Vec<PathSegment> },
2002}
2003
2004impl ShapeBoundary {
2005 pub fn inflate(&self, margin: f32) -> Self {
2006 if margin == 0.0 {
2007 return self.clone();
2008 }
2009 match self {
2010 Self::Rectangle(rect) => Self::Rectangle(Rect {
2011 x: rect.x - margin,
2012 y: rect.y - margin,
2013 width: (rect.width + margin * 2.0).max(0.0),
2014 height: (rect.height + margin * 2.0).max(0.0),
2015 }),
2016 Self::Circle { center, radius } => Self::Circle {
2017 center: *center,
2018 radius: radius + margin,
2019 },
2020 _ => self.clone(),
2023 }
2024 }
2025}
2026
2027impl Hash for ShapeBoundary {
2029 fn hash<H: Hasher>(&self, state: &mut H) {
2030 discriminant(self).hash(state);
2031 match self {
2032 ShapeBoundary::Rectangle(rect) => rect.hash(state),
2033 ShapeBoundary::Circle { center, radius } => {
2034 center.hash(state);
2035 (radius.round() as usize).hash(state);
2036 }
2037 ShapeBoundary::Ellipse { center, radii } => {
2038 center.hash(state);
2039 radii.hash(state);
2040 }
2041 ShapeBoundary::Polygon { points } => points.hash(state),
2042 ShapeBoundary::Path { segments } => segments.hash(state),
2043 }
2044 }
2045}
2046impl PartialEq for ShapeBoundary {
2047 fn eq(&self, other: &Self) -> bool {
2048 match (self, other) {
2049 (ShapeBoundary::Rectangle(r1), ShapeBoundary::Rectangle(r2)) => r1 == r2,
2050 (
2051 ShapeBoundary::Circle {
2052 center: c1,
2053 radius: r1,
2054 },
2055 ShapeBoundary::Circle {
2056 center: c2,
2057 radius: r2,
2058 },
2059 ) => c1 == c2 && round_eq(*r1, *r2),
2060 (
2061 ShapeBoundary::Ellipse {
2062 center: c1,
2063 radii: r1,
2064 },
2065 ShapeBoundary::Ellipse {
2066 center: c2,
2067 radii: r2,
2068 },
2069 ) => c1 == c2 && r1 == r2,
2070 (ShapeBoundary::Polygon { points: p1 }, ShapeBoundary::Polygon { points: p2 }) => {
2071 p1 == p2
2072 }
2073 (ShapeBoundary::Path { segments: s1 }, ShapeBoundary::Path { segments: s2 }) => {
2074 s1 == s2
2075 }
2076 _ => false,
2077 }
2078 }
2079}
2080impl Eq for ShapeBoundary {}
2081
2082impl ShapeBoundary {
2083 pub fn from_css_shape(
2092 css_shape: &azul_css::shape::CssShape,
2093 reference_box: Rect,
2094 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2095 ) -> Self {
2096 use azul_css::shape::CssShape;
2097
2098 if let Some(msgs) = debug_messages {
2099 msgs.push(LayoutDebugMessage::info(format!(
2100 "[ShapeBoundary::from_css_shape] Input CSS shape: {:?}",
2101 css_shape
2102 )));
2103 msgs.push(LayoutDebugMessage::info(format!(
2104 "[ShapeBoundary::from_css_shape] Reference box: {:?}",
2105 reference_box
2106 )));
2107 }
2108
2109 let result = match css_shape {
2110 CssShape::Circle(circle) => {
2111 let center = Point {
2112 x: reference_box.x + circle.center.x,
2113 y: reference_box.y + circle.center.y,
2114 };
2115 if let Some(msgs) = debug_messages {
2116 msgs.push(LayoutDebugMessage::info(format!(
2117 "[ShapeBoundary::from_css_shape] Circle - CSS center: ({}, {}), radius: {}",
2118 circle.center.x, circle.center.y, circle.radius
2119 )));
2120 msgs.push(LayoutDebugMessage::info(format!(
2121 "[ShapeBoundary::from_css_shape] Circle - Absolute center: ({}, {}), \
2122 radius: {}",
2123 center.x, center.y, circle.radius
2124 )));
2125 }
2126 ShapeBoundary::Circle {
2127 center,
2128 radius: circle.radius,
2129 }
2130 }
2131
2132 CssShape::Ellipse(ellipse) => {
2133 let center = Point {
2134 x: reference_box.x + ellipse.center.x,
2135 y: reference_box.y + ellipse.center.y,
2136 };
2137 let radii = Size {
2138 width: ellipse.radius_x,
2139 height: ellipse.radius_y,
2140 };
2141 if let Some(msgs) = debug_messages {
2142 msgs.push(LayoutDebugMessage::info(format!(
2143 "[ShapeBoundary::from_css_shape] Ellipse - center: ({}, {}), radii: ({}, \
2144 {})",
2145 center.x, center.y, radii.width, radii.height
2146 )));
2147 }
2148 ShapeBoundary::Ellipse { center, radii }
2149 }
2150
2151 CssShape::Polygon(polygon) => {
2152 let points = polygon
2153 .points
2154 .as_ref()
2155 .iter()
2156 .map(|pt| Point {
2157 x: reference_box.x + pt.x,
2158 y: reference_box.y + pt.y,
2159 })
2160 .collect();
2161 if let Some(msgs) = debug_messages {
2162 msgs.push(LayoutDebugMessage::info(format!(
2163 "[ShapeBoundary::from_css_shape] Polygon - {} points",
2164 polygon.points.as_ref().len()
2165 )));
2166 }
2167 ShapeBoundary::Polygon { points }
2168 }
2169
2170 CssShape::Inset(inset) => {
2171 let x = reference_box.x + inset.inset_left;
2173 let y = reference_box.y + inset.inset_top;
2174 let width = reference_box.width - inset.inset_left - inset.inset_right;
2175 let height = reference_box.height - inset.inset_top - inset.inset_bottom;
2176
2177 if let Some(msgs) = debug_messages {
2178 msgs.push(LayoutDebugMessage::info(format!(
2179 "[ShapeBoundary::from_css_shape] Inset - insets: ({}, {}, {}, {})",
2180 inset.inset_top, inset.inset_right, inset.inset_bottom, inset.inset_left
2181 )));
2182 msgs.push(LayoutDebugMessage::info(format!(
2183 "[ShapeBoundary::from_css_shape] Inset - resulting rect: x={}, y={}, \
2184 w={}, h={}",
2185 x, y, width, height
2186 )));
2187 }
2188
2189 ShapeBoundary::Rectangle(Rect {
2190 x,
2191 y,
2192 width: width.max(0.0),
2193 height: height.max(0.0),
2194 })
2195 }
2196
2197 CssShape::Path(path) => {
2198 if let Some(msgs) = debug_messages {
2199 msgs.push(LayoutDebugMessage::info(
2200 "[ShapeBoundary::from_css_shape] Path - fallback to rectangle".to_string(),
2201 ));
2202 }
2203 ShapeBoundary::Rectangle(reference_box)
2206 }
2207 };
2208
2209 if let Some(msgs) = debug_messages {
2210 msgs.push(LayoutDebugMessage::info(format!(
2211 "[ShapeBoundary::from_css_shape] Result: {:?}",
2212 result
2213 )));
2214 }
2215 result
2216 }
2217}
2218
2219#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2220pub struct InlineBreak {
2221 pub break_type: BreakType,
2222 pub clear: ClearType,
2223 pub content_index: usize,
2224}
2225
2226#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2227pub enum BreakType {
2228 Soft, Hard, Page, Column, }
2233
2234#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2235pub enum ClearType {
2236 None,
2237 Left,
2238 Right,
2239 Both,
2240}
2241
2242#[derive(Debug, Clone)]
2244pub struct ShapeConstraints {
2245 pub boundaries: Vec<ShapeBoundary>,
2246 pub exclusions: Vec<ShapeBoundary>,
2247 pub writing_mode: WritingMode,
2248 pub text_align: TextAlign,
2249 pub line_height: f32,
2250}
2251
2252#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
2253pub enum WritingMode {
2254 #[default]
2255 HorizontalTb, VerticalRl, VerticalLr, SidewaysRl, SidewaysLr, }
2261
2262impl WritingMode {
2263 pub fn is_advance_horizontal(&self) -> bool {
2265 matches!(
2266 self,
2267 WritingMode::HorizontalTb | WritingMode::SidewaysRl | WritingMode::SidewaysLr
2268 )
2269 }
2270}
2271
2272#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
2273pub enum JustifyContent {
2274 #[default]
2275 None,
2276 InterWord, InterCharacter, Distribute, Kashida, }
2281
2282#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
2284pub enum TextAlign {
2285 #[default]
2286 Left,
2287 Right,
2288 Center,
2289 Justify,
2290 Start,
2291 End, JustifyAll, }
2294
2295#[derive(Debug, Clone, Copy, PartialEq, Default, Eq, PartialOrd, Ord, Hash)]
2297pub enum TextOrientation {
2298 #[default]
2299 Mixed, Upright, Sideways, }
2303
2304#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2305pub struct TextDecoration {
2306 pub underline: bool,
2307 pub strikethrough: bool,
2308 pub overline: bool,
2309}
2310
2311impl Default for TextDecoration {
2312 fn default() -> Self {
2313 TextDecoration {
2314 underline: false,
2315 overline: false,
2316 strikethrough: false,
2317 }
2318 }
2319}
2320
2321impl TextDecoration {
2322 pub fn from_css(css: azul_css::props::style::text::StyleTextDecoration) -> Self {
2328 use azul_css::props::style::text::StyleTextDecoration;
2329 match css {
2330 StyleTextDecoration::None => Self::default(),
2331 StyleTextDecoration::Underline => Self {
2332 underline: true,
2333 strikethrough: false,
2334 overline: false,
2335 },
2336 StyleTextDecoration::Overline => Self {
2337 underline: false,
2338 strikethrough: false,
2339 overline: true,
2340 },
2341 StyleTextDecoration::LineThrough => Self {
2342 underline: false,
2343 strikethrough: true,
2344 overline: false,
2345 },
2346 }
2347 }
2348}
2349
2350#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2351pub enum TextTransform {
2352 #[default]
2353 None,
2354 Uppercase,
2355 Lowercase,
2356 Capitalize,
2357}
2358
2359pub type FourCc = [u8; 4];
2361
2362#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
2364pub enum Spacing {
2365 Px(i32), Em(f32),
2367}
2368
2369impl Eq for Spacing {}
2373
2374impl Hash for Spacing {
2375 fn hash<H: Hasher>(&self, state: &mut H) {
2376 discriminant(self).hash(state);
2378 match self {
2379 Spacing::Px(val) => val.hash(state),
2380 Spacing::Em(val) => val.to_bits().hash(state),
2383 }
2384 }
2385}
2386
2387impl Default for Spacing {
2388 fn default() -> Self {
2389 Spacing::Px(0)
2390 }
2391}
2392
2393impl Default for FontHash {
2394 fn default() -> Self {
2395 Self::invalid()
2396 }
2397}
2398
2399#[derive(Debug, Clone, PartialEq)]
2401pub struct StyleProperties {
2402 pub font_stack: FontStack,
2406 pub font_size_px: f32,
2407 pub color: ColorU,
2408 pub background_color: Option<ColorU>,
2419 pub background_content: Vec<StyleBackgroundContent>,
2422 pub border: Option<InlineBorderInfo>,
2424 pub letter_spacing: Spacing,
2425 pub word_spacing: Spacing,
2426
2427 pub line_height: f32,
2428 pub text_decoration: TextDecoration,
2429
2430 pub font_features: Vec<String>,
2432
2433 pub font_variations: Vec<(FourCc, f32)>,
2435 pub tab_size: f32,
2437 pub text_transform: TextTransform,
2439 pub writing_mode: WritingMode,
2441 pub text_orientation: TextOrientation,
2442 pub text_combine_upright: Option<TextCombineUpright>,
2444
2445 pub font_variant_caps: FontVariantCaps,
2447 pub font_variant_numeric: FontVariantNumeric,
2448 pub font_variant_ligatures: FontVariantLigatures,
2449 pub font_variant_east_asian: FontVariantEastAsian,
2450}
2451
2452impl Default for StyleProperties {
2453 fn default() -> Self {
2454 const FONT_SIZE: f32 = 16.0;
2455 const TAB_SIZE: f32 = 8.0;
2456 Self {
2457 font_stack: FontStack::default(),
2458 font_size_px: FONT_SIZE,
2459 color: ColorU::default(),
2460 background_color: None,
2461 background_content: Vec::new(),
2462 border: None,
2463 letter_spacing: Spacing::default(), word_spacing: Spacing::default(), line_height: FONT_SIZE * 1.2,
2466 text_decoration: TextDecoration::default(),
2467 font_features: Vec::new(),
2468 font_variations: Vec::new(),
2469 tab_size: TAB_SIZE, text_transform: TextTransform::default(),
2471 writing_mode: WritingMode::default(),
2472 text_orientation: TextOrientation::default(),
2473 text_combine_upright: None,
2474 font_variant_caps: FontVariantCaps::default(),
2475 font_variant_numeric: FontVariantNumeric::default(),
2476 font_variant_ligatures: FontVariantLigatures::default(),
2477 font_variant_east_asian: FontVariantEastAsian::default(),
2478 }
2479 }
2480}
2481
2482impl Hash for StyleProperties {
2483 fn hash<H: Hasher>(&self, state: &mut H) {
2484 self.font_stack.hash(state);
2485 self.color.hash(state);
2486 self.background_color.hash(state);
2487 self.text_decoration.hash(state);
2488 self.font_features.hash(state);
2489 self.writing_mode.hash(state);
2490 self.text_orientation.hash(state);
2491 self.text_combine_upright.hash(state);
2492 self.letter_spacing.hash(state);
2493 self.word_spacing.hash(state);
2494
2495 (self.font_size_px.round() as usize).hash(state);
2497 (self.line_height.round() as usize).hash(state);
2498 }
2499}
2500
2501impl StyleProperties {
2502 pub fn layout_hash(&self) -> u64 {
2519 use std::hash::Hasher;
2520 let mut hasher = std::collections::hash_map::DefaultHasher::new();
2521
2522 self.font_stack.hash(&mut hasher);
2524 (self.font_size_px.round() as usize).hash(&mut hasher);
2525 self.font_features.hash(&mut hasher);
2526 for (tag, value) in &self.font_variations {
2528 tag.hash(&mut hasher);
2529 (value.round() as i32).hash(&mut hasher);
2530 }
2531
2532 self.letter_spacing.hash(&mut hasher);
2534 self.word_spacing.hash(&mut hasher);
2535 (self.line_height.round() as usize).hash(&mut hasher);
2536 (self.tab_size.round() as usize).hash(&mut hasher);
2537
2538 self.writing_mode.hash(&mut hasher);
2540 self.text_orientation.hash(&mut hasher);
2541 self.text_combine_upright.hash(&mut hasher);
2542
2543 self.text_transform.hash(&mut hasher);
2545
2546 self.font_variant_caps.hash(&mut hasher);
2548 self.font_variant_numeric.hash(&mut hasher);
2549 self.font_variant_ligatures.hash(&mut hasher);
2550 self.font_variant_east_asian.hash(&mut hasher);
2551
2552 hasher.finish()
2553 }
2554
2555 pub fn layout_eq(&self, other: &Self) -> bool {
2559 self.layout_hash() == other.layout_hash()
2560 }
2561}
2562
2563#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
2564pub enum TextCombineUpright {
2565 None,
2566 All, Digits(u8), }
2569
2570#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2571pub enum GlyphSource {
2572 Char,
2574 Hyphen,
2576}
2577
2578#[derive(Debug, Clone, Copy, PartialEq)]
2579pub enum CharacterClass {
2580 Space, Punctuation, Letter, Ideograph, Symbol, Combining, }
2587
2588#[derive(Debug, Clone, Copy, PartialEq)]
2589pub enum GlyphOrientation {
2590 Horizontal, Vertical, Upright, Mixed, }
2595
2596#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2598pub enum BidiDirection {
2599 Ltr,
2600 Rtl,
2601}
2602
2603impl BidiDirection {
2604 pub fn is_rtl(&self) -> bool {
2605 matches!(self, BidiDirection::Rtl)
2606 }
2607}
2608
2609#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2610pub enum FontVariantCaps {
2611 #[default]
2612 Normal,
2613 SmallCaps,
2614 AllSmallCaps,
2615 PetiteCaps,
2616 AllPetiteCaps,
2617 Unicase,
2618 TitlingCaps,
2619}
2620
2621#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2622pub enum FontVariantNumeric {
2623 #[default]
2624 Normal,
2625 LiningNums,
2626 OldstyleNums,
2627 ProportionalNums,
2628 TabularNums,
2629 DiagonalFractions,
2630 StackedFractions,
2631 Ordinal,
2632 SlashedZero,
2633}
2634
2635#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2636pub enum FontVariantLigatures {
2637 #[default]
2638 Normal,
2639 None,
2640 Common,
2641 NoCommon,
2642 Discretionary,
2643 NoDiscretionary,
2644 Historical,
2645 NoHistorical,
2646 Contextual,
2647 NoContextual,
2648}
2649
2650#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2651pub enum FontVariantEastAsian {
2652 #[default]
2653 Normal,
2654 Jis78,
2655 Jis83,
2656 Jis90,
2657 Jis04,
2658 Simplified,
2659 Traditional,
2660 FullWidth,
2661 ProportionalWidth,
2662 Ruby,
2663}
2664
2665#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2666pub struct BidiLevel(u8);
2667
2668impl BidiLevel {
2669 pub fn new(level: u8) -> Self {
2670 Self(level)
2671 }
2672 pub fn is_rtl(&self) -> bool {
2673 self.0 % 2 == 1
2674 }
2675 pub fn level(&self) -> u8 {
2676 self.0
2677 }
2678}
2679
2680#[derive(Debug, Clone)]
2682pub struct StyleOverride {
2683 pub target: ContentIndex,
2685 pub style: PartialStyleProperties,
2688}
2689
2690#[derive(Debug, Clone, Default)]
2691pub struct PartialStyleProperties {
2692 pub font_stack: Option<FontStack>,
2693 pub font_size_px: Option<f32>,
2694 pub color: Option<ColorU>,
2695 pub letter_spacing: Option<Spacing>,
2696 pub word_spacing: Option<Spacing>,
2697 pub line_height: Option<f32>,
2698 pub text_decoration: Option<TextDecoration>,
2699 pub font_features: Option<Vec<String>>,
2700 pub font_variations: Option<Vec<(FourCc, f32)>>,
2701 pub tab_size: Option<f32>,
2702 pub text_transform: Option<TextTransform>,
2703 pub writing_mode: Option<WritingMode>,
2704 pub text_orientation: Option<TextOrientation>,
2705 pub text_combine_upright: Option<Option<TextCombineUpright>>,
2706 pub font_variant_caps: Option<FontVariantCaps>,
2707 pub font_variant_numeric: Option<FontVariantNumeric>,
2708 pub font_variant_ligatures: Option<FontVariantLigatures>,
2709 pub font_variant_east_asian: Option<FontVariantEastAsian>,
2710}
2711
2712impl Hash for PartialStyleProperties {
2713 fn hash<H: Hasher>(&self, state: &mut H) {
2714 self.font_stack.hash(state);
2715 self.font_size_px.map(|f| f.to_bits()).hash(state);
2716 self.color.hash(state);
2717 self.letter_spacing.hash(state);
2718 self.word_spacing.hash(state);
2719 self.line_height.map(|f| f.to_bits()).hash(state);
2720 self.text_decoration.hash(state);
2721 self.font_features.hash(state);
2722
2723 self.font_variations.as_ref().map(|v| {
2725 for (tag, val) in v {
2726 tag.hash(state);
2727 val.to_bits().hash(state);
2728 }
2729 });
2730
2731 self.tab_size.map(|f| f.to_bits()).hash(state);
2732 self.text_transform.hash(state);
2733 self.writing_mode.hash(state);
2734 self.text_orientation.hash(state);
2735 self.text_combine_upright.hash(state);
2736 self.font_variant_caps.hash(state);
2737 self.font_variant_numeric.hash(state);
2738 self.font_variant_ligatures.hash(state);
2739 self.font_variant_east_asian.hash(state);
2740 }
2741}
2742
2743impl PartialEq for PartialStyleProperties {
2744 fn eq(&self, other: &Self) -> bool {
2745 self.font_stack == other.font_stack &&
2746 self.font_size_px.map(|f| f.to_bits()) == other.font_size_px.map(|f| f.to_bits()) &&
2747 self.color == other.color &&
2748 self.letter_spacing == other.letter_spacing &&
2749 self.word_spacing == other.word_spacing &&
2750 self.line_height.map(|f| f.to_bits()) == other.line_height.map(|f| f.to_bits()) &&
2751 self.text_decoration == other.text_decoration &&
2752 self.font_features == other.font_features &&
2753 self.font_variations == other.font_variations && self.tab_size.map(|f| f.to_bits()) == other.tab_size.map(|f| f.to_bits()) &&
2755 self.text_transform == other.text_transform &&
2756 self.writing_mode == other.writing_mode &&
2757 self.text_orientation == other.text_orientation &&
2758 self.text_combine_upright == other.text_combine_upright &&
2759 self.font_variant_caps == other.font_variant_caps &&
2760 self.font_variant_numeric == other.font_variant_numeric &&
2761 self.font_variant_ligatures == other.font_variant_ligatures &&
2762 self.font_variant_east_asian == other.font_variant_east_asian
2763 }
2764}
2765
2766impl Eq for PartialStyleProperties {}
2767
2768impl StyleProperties {
2769 fn apply_override(&self, partial: &PartialStyleProperties) -> Self {
2770 let mut new_style = self.clone();
2771 if let Some(val) = &partial.font_stack {
2772 new_style.font_stack = val.clone();
2773 }
2774 if let Some(val) = partial.font_size_px {
2775 new_style.font_size_px = val;
2776 }
2777 if let Some(val) = &partial.color {
2778 new_style.color = val.clone();
2779 }
2780 if let Some(val) = partial.letter_spacing {
2781 new_style.letter_spacing = val;
2782 }
2783 if let Some(val) = partial.word_spacing {
2784 new_style.word_spacing = val;
2785 }
2786 if let Some(val) = partial.line_height {
2787 new_style.line_height = val;
2788 }
2789 if let Some(val) = &partial.text_decoration {
2790 new_style.text_decoration = val.clone();
2791 }
2792 if let Some(val) = &partial.font_features {
2793 new_style.font_features = val.clone();
2794 }
2795 if let Some(val) = &partial.font_variations {
2796 new_style.font_variations = val.clone();
2797 }
2798 if let Some(val) = partial.tab_size {
2799 new_style.tab_size = val;
2800 }
2801 if let Some(val) = partial.text_transform {
2802 new_style.text_transform = val;
2803 }
2804 if let Some(val) = partial.writing_mode {
2805 new_style.writing_mode = val;
2806 }
2807 if let Some(val) = partial.text_orientation {
2808 new_style.text_orientation = val;
2809 }
2810 if let Some(val) = &partial.text_combine_upright {
2811 new_style.text_combine_upright = val.clone();
2812 }
2813 if let Some(val) = partial.font_variant_caps {
2814 new_style.font_variant_caps = val;
2815 }
2816 if let Some(val) = partial.font_variant_numeric {
2817 new_style.font_variant_numeric = val;
2818 }
2819 if let Some(val) = partial.font_variant_ligatures {
2820 new_style.font_variant_ligatures = val;
2821 }
2822 if let Some(val) = partial.font_variant_east_asian {
2823 new_style.font_variant_east_asian = val;
2824 }
2825 new_style
2826 }
2827}
2828
2829#[derive(Debug, Clone, Copy, PartialEq)]
2831pub enum GlyphKind {
2832 Character,
2834 Hyphen,
2836 NotDef,
2838 Kashida {
2840 width: f32,
2842 },
2843}
2844
2845#[derive(Debug, Clone)]
2848pub enum LogicalItem {
2849 Text {
2850 source: ContentIndex,
2852 text: String,
2854 style: Arc<StyleProperties>,
2855 marker_position_outside: Option<bool>,
2859 source_node_id: Option<NodeId>,
2862 },
2863 CombinedText {
2865 source: ContentIndex,
2866 text: String,
2867 style: Arc<StyleProperties>,
2868 },
2869 Ruby {
2870 source: ContentIndex,
2871 base_text: String,
2874 ruby_text: String,
2875 style: Arc<StyleProperties>,
2876 },
2877 Object {
2878 source: ContentIndex,
2880 content: InlineContent,
2882 },
2883 Tab {
2884 source: ContentIndex,
2885 style: Arc<StyleProperties>,
2886 },
2887 Break {
2888 source: ContentIndex,
2889 break_info: InlineBreak,
2890 },
2891}
2892
2893impl Hash for LogicalItem {
2894 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2895 discriminant(self).hash(state);
2896 match self {
2897 LogicalItem::Text {
2898 source,
2899 text,
2900 style,
2901 marker_position_outside,
2902 source_node_id,
2903 } => {
2904 source.hash(state);
2905 text.hash(state);
2906 style.as_ref().hash(state); marker_position_outside.hash(state);
2908 source_node_id.hash(state);
2909 }
2910 LogicalItem::CombinedText {
2911 source,
2912 text,
2913 style,
2914 } => {
2915 source.hash(state);
2916 text.hash(state);
2917 style.as_ref().hash(state);
2918 }
2919 LogicalItem::Ruby {
2920 source,
2921 base_text,
2922 ruby_text,
2923 style,
2924 } => {
2925 source.hash(state);
2926 base_text.hash(state);
2927 ruby_text.hash(state);
2928 style.as_ref().hash(state);
2929 }
2930 LogicalItem::Object { source, content } => {
2931 source.hash(state);
2932 content.hash(state);
2933 }
2934 LogicalItem::Tab { source, style } => {
2935 source.hash(state);
2936 style.as_ref().hash(state);
2937 }
2938 LogicalItem::Break { source, break_info } => {
2939 source.hash(state);
2940 break_info.hash(state);
2941 }
2942 }
2943 }
2944}
2945
2946#[derive(Debug, Clone)]
2949pub struct VisualItem {
2950 pub logical_source: LogicalItem,
2953 pub bidi_level: BidiLevel,
2955 pub script: Script,
2957 pub text: String,
2959}
2960
2961#[derive(Debug, Clone)]
2964pub enum ShapedItem {
2965 Cluster(ShapedCluster),
2966 CombinedBlock {
2969 source: ContentIndex,
2970 glyphs: Vec<ShapedGlyph>,
2972 bounds: Rect,
2973 baseline_offset: f32,
2974 },
2975 Object {
2976 source: ContentIndex,
2977 bounds: Rect,
2978 baseline_offset: f32,
2979 content: InlineContent,
2981 },
2982 Tab {
2983 source: ContentIndex,
2984 bounds: Rect,
2985 },
2986 Break {
2987 source: ContentIndex,
2988 break_info: InlineBreak,
2989 },
2990}
2991
2992impl ShapedItem {
2993 pub fn as_cluster(&self) -> Option<&ShapedCluster> {
2994 match self {
2995 ShapedItem::Cluster(c) => Some(c),
2996 _ => None,
2997 }
2998 }
2999 pub fn bounds(&self) -> Rect {
3005 match self {
3006 ShapedItem::Cluster(cluster) => {
3007 let width = cluster.advance;
3009
3010 let (ascent, descent) = get_item_vertical_metrics(self);
3014 let height = ascent + descent;
3015
3016 Rect {
3017 x: 0.0,
3018 y: 0.0,
3019 width,
3020 height,
3021 }
3022 }
3023 ShapedItem::CombinedBlock { bounds, .. } => *bounds,
3026 ShapedItem::Object { bounds, .. } => *bounds,
3027 ShapedItem::Tab { bounds, .. } => *bounds,
3028
3029 ShapedItem::Break { .. } => Rect::default(), }
3032 }
3033}
3034
3035#[derive(Debug, Clone)]
3037pub struct ShapedCluster {
3038 pub text: String,
3041 pub source_cluster_id: GraphemeClusterId,
3043 pub source_content_index: ContentIndex,
3045 pub source_node_id: Option<NodeId>,
3048 pub glyphs: Vec<ShapedGlyph>,
3050 pub advance: f32,
3052 pub direction: BidiDirection,
3054 pub style: Arc<StyleProperties>,
3056 pub marker_position_outside: Option<bool>,
3060}
3061
3062#[derive(Debug, Clone)]
3064pub struct ShapedGlyph {
3065 pub kind: GlyphKind,
3067 pub glyph_id: u16,
3069 pub cluster_offset: u32,
3071 pub advance: f32,
3074 pub kerning: f32,
3077 pub offset: Point,
3079 pub vertical_advance: f32,
3081 pub vertical_offset: Point,
3083 pub script: Script,
3084 pub style: Arc<StyleProperties>,
3085 pub font_hash: u64,
3087 pub font_metrics: LayoutFontMetrics,
3089}
3090
3091impl ShapedGlyph {
3092 pub fn into_glyph_instance<T: ParsedFontTrait>(
3093 &self,
3094 writing_mode: WritingMode,
3095 loaded_fonts: &LoadedFonts<T>,
3096 ) -> GlyphInstance {
3097 let size = loaded_fonts
3098 .get_by_hash(self.font_hash)
3099 .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
3100 .unwrap_or_default();
3101
3102 let position = if writing_mode.is_advance_horizontal() {
3103 LogicalPosition {
3104 x: self.offset.x,
3105 y: self.offset.y,
3106 }
3107 } else {
3108 LogicalPosition {
3109 x: self.vertical_offset.x,
3110 y: self.vertical_offset.y,
3111 }
3112 };
3113
3114 GlyphInstance {
3115 index: self.glyph_id as u32,
3116 point: position,
3117 size,
3118 }
3119 }
3120
3121 pub fn into_glyph_instance_at<T: ParsedFontTrait>(
3124 &self,
3125 writing_mode: WritingMode,
3126 absolute_position: LogicalPosition,
3127 loaded_fonts: &LoadedFonts<T>,
3128 ) -> GlyphInstance {
3129 let size = loaded_fonts
3130 .get_by_hash(self.font_hash)
3131 .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
3132 .unwrap_or_default();
3133
3134 GlyphInstance {
3135 index: self.glyph_id as u32,
3136 point: absolute_position,
3137 size,
3138 }
3139 }
3140
3141 pub fn into_glyph_instance_at_simple(
3145 &self,
3146 _writing_mode: WritingMode,
3147 absolute_position: LogicalPosition,
3148 ) -> GlyphInstance {
3149 GlyphInstance {
3152 index: self.glyph_id as u32,
3153 point: absolute_position,
3154 size: LogicalSize::default(),
3155 }
3156 }
3157}
3158
3159#[derive(Debug, Clone)]
3162pub struct PositionedItem {
3163 pub item: ShapedItem,
3164 pub position: Point,
3165 pub line_index: usize,
3166}
3167
3168#[derive(Debug, Clone)]
3169pub struct UnifiedLayout {
3170 pub items: Vec<PositionedItem>,
3171 pub overflow: OverflowInfo,
3173}
3174
3175impl UnifiedLayout {
3176 pub fn bounds(&self) -> Rect {
3179 if self.items.is_empty() {
3180 return Rect::default();
3181 }
3182
3183 let mut min_x = f32::MAX;
3184 let mut min_y = f32::MAX;
3185 let mut max_x = f32::MIN;
3186 let mut max_y = f32::MIN;
3187
3188 for item in &self.items {
3189 let item_x = item.position.x;
3190 let item_y = item.position.y;
3191
3192 let item_bounds = item.item.bounds();
3194 let item_width = item_bounds.width;
3195 let item_height = item_bounds.height;
3196
3197 min_x = min_x.min(item_x);
3198 min_y = min_y.min(item_y);
3199 max_x = max_x.max(item_x + item_width);
3200 max_y = max_y.max(item_y + item_height);
3201 }
3202
3203 Rect {
3204 x: min_x,
3205 y: min_y,
3206 width: max_x - min_x,
3207 height: max_y - min_y,
3208 }
3209 }
3210
3211 pub fn is_empty(&self) -> bool {
3212 self.items.is_empty()
3213 }
3214 pub fn last_baseline(&self) -> Option<f32> {
3215 self.items
3216 .iter()
3217 .rev()
3218 .find_map(|item| get_baseline_for_item(&item.item))
3219 }
3220
3221 pub fn hittest_cursor(&self, point: LogicalPosition) -> Option<TextCursor> {
3227 if self.items.is_empty() {
3228 return None;
3229 }
3230
3231 let mut closest_item_idx = 0;
3233 let mut closest_distance = f32::MAX;
3234
3235 for (idx, item) in self.items.iter().enumerate() {
3236 if !matches!(item.item, ShapedItem::Cluster(_)) {
3238 continue;
3239 }
3240
3241 let item_bounds = item.item.bounds();
3242 let item_center_y = item.position.y + item_bounds.height / 2.0;
3243
3244 let vertical_distance = (point.y - item_center_y).abs();
3246
3247 let horizontal_distance = if point.x < item.position.x {
3249 item.position.x - point.x
3250 } else if point.x > item.position.x + item_bounds.width {
3251 point.x - (item.position.x + item_bounds.width)
3252 } else {
3253 0.0 };
3255
3256 let distance = vertical_distance * 2.0 + horizontal_distance;
3258
3259 if distance < closest_distance {
3260 closest_distance = distance;
3261 closest_item_idx = idx;
3262 }
3263 }
3264
3265 let closest_item = &self.items[closest_item_idx];
3267 let cluster = match &closest_item.item {
3268 ShapedItem::Cluster(c) => c,
3269 ShapedItem::Object { source, .. } | ShapedItem::CombinedBlock { source, .. } => {
3271 return Some(TextCursor {
3272 cluster_id: GraphemeClusterId {
3273 source_run: source.run_index,
3274 start_byte_in_run: source.item_index,
3275 },
3276 affinity: if point.x
3277 < closest_item.position.x + (closest_item.item.bounds().width / 2.0)
3278 {
3279 CursorAffinity::Leading
3280 } else {
3281 CursorAffinity::Trailing
3282 },
3283 });
3284 }
3285 _ => return None,
3286 };
3287
3288 let cluster_mid_x = closest_item.position.x + cluster.advance / 2.0;
3290 let affinity = if point.x < cluster_mid_x {
3291 CursorAffinity::Leading
3292 } else {
3293 CursorAffinity::Trailing
3294 };
3295
3296 Some(TextCursor {
3297 cluster_id: cluster.source_cluster_id,
3298 affinity,
3299 })
3300 }
3301
3302 pub fn get_selection_rects(&self, range: &SelectionRange) -> Vec<LogicalRect> {
3305 let mut cluster_map: HashMap<GraphemeClusterId, &PositionedItem> = HashMap::new();
3307 for item in &self.items {
3308 if let Some(cluster) = item.item.as_cluster() {
3309 cluster_map.insert(cluster.source_cluster_id, item);
3310 }
3311 }
3312
3313 let (start_cursor, end_cursor) = if range.start.cluster_id > range.end.cluster_id
3315 || (range.start.cluster_id == range.end.cluster_id
3316 && range.start.affinity > range.end.affinity)
3317 {
3318 (range.end, range.start)
3319 } else {
3320 (range.start, range.end)
3321 };
3322
3323 let Some(start_item) = cluster_map.get(&start_cursor.cluster_id) else {
3325 return Vec::new();
3326 };
3327 let Some(end_item) = cluster_map.get(&end_cursor.cluster_id) else {
3328 return Vec::new();
3329 };
3330
3331 let mut rects = Vec::new();
3332
3333 let get_cursor_x = |item: &PositionedItem, affinity: CursorAffinity| -> f32 {
3335 match affinity {
3336 CursorAffinity::Leading => item.position.x,
3337 CursorAffinity::Trailing => item.position.x + get_item_measure(&item.item, false),
3338 }
3339 };
3340
3341 let get_line_bounds = |line_index: usize| -> Option<LogicalRect> {
3343 let items_on_line = self.items.iter().filter(|i| i.line_index == line_index);
3344
3345 let mut min_x: Option<f32> = None;
3346 let mut max_x: Option<f32> = None;
3347 let mut min_y: Option<f32> = None;
3348 let mut max_y: Option<f32> = None;
3349
3350 for item in items_on_line {
3351 let item_bounds = item.item.bounds();
3353 if item_bounds.width <= 0.0 && item_bounds.height <= 0.0 {
3354 continue;
3355 }
3356
3357 let item_x_end = item.position.x + item_bounds.width;
3358 let item_y_end = item.position.y + item_bounds.height;
3359
3360 min_x = Some(min_x.map_or(item.position.x, |mx| mx.min(item.position.x)));
3361 max_x = Some(max_x.map_or(item_x_end, |mx| mx.max(item_x_end)));
3362 min_y = Some(min_y.map_or(item.position.y, |my| my.min(item.position.y)));
3363 max_y = Some(max_y.map_or(item_y_end, |my| my.max(item_y_end)));
3364 }
3365
3366 if let (Some(min_x), Some(max_x), Some(min_y), Some(max_y)) =
3367 (min_x, max_x, min_y, max_y)
3368 {
3369 Some(LogicalRect {
3370 origin: LogicalPosition { x: min_x, y: min_y },
3371 size: LogicalSize {
3372 width: max_x - min_x,
3373 height: max_y - min_y,
3374 },
3375 })
3376 } else {
3377 None
3378 }
3379 };
3380
3381 if start_item.line_index == end_item.line_index {
3383 if let Some(line_bounds) = get_line_bounds(start_item.line_index) {
3384 let start_x = get_cursor_x(start_item, start_cursor.affinity);
3385 let end_x = get_cursor_x(end_item, end_cursor.affinity);
3386
3387 rects.push(LogicalRect {
3389 origin: LogicalPosition {
3390 x: start_x.min(end_x),
3391 y: line_bounds.origin.y,
3392 },
3393 size: LogicalSize {
3394 width: (end_x - start_x).abs(),
3395 height: line_bounds.size.height,
3396 },
3397 });
3398 }
3399 }
3400 else {
3402 if let Some(start_line_bounds) = get_line_bounds(start_item.line_index) {
3404 let start_x = get_cursor_x(start_item, start_cursor.affinity);
3405 let line_end_x = start_line_bounds.origin.x + start_line_bounds.size.width;
3406 rects.push(LogicalRect {
3407 origin: LogicalPosition {
3408 x: start_x,
3409 y: start_line_bounds.origin.y,
3410 },
3411 size: LogicalSize {
3412 width: line_end_x - start_x,
3413 height: start_line_bounds.size.height,
3414 },
3415 });
3416 }
3417
3418 for line_idx in (start_item.line_index + 1)..end_item.line_index {
3420 if let Some(line_bounds) = get_line_bounds(line_idx) {
3421 rects.push(line_bounds);
3422 }
3423 }
3424
3425 if let Some(end_line_bounds) = get_line_bounds(end_item.line_index) {
3427 let line_start_x = end_line_bounds.origin.x;
3428 let end_x = get_cursor_x(end_item, end_cursor.affinity);
3429 rects.push(LogicalRect {
3430 origin: LogicalPosition {
3431 x: line_start_x,
3432 y: end_line_bounds.origin.y,
3433 },
3434 size: LogicalSize {
3435 width: end_x - line_start_x,
3436 height: end_line_bounds.size.height,
3437 },
3438 });
3439 }
3440 }
3441
3442 rects
3443 }
3444
3445 pub fn get_cursor_rect(&self, cursor: &TextCursor) -> Option<LogicalRect> {
3447 for item in &self.items {
3449 if let ShapedItem::Cluster(cluster) = &item.item {
3450 if cluster.source_cluster_id == cursor.cluster_id {
3451 let line_height = item.item.bounds().height;
3453 let cursor_x = match cursor.affinity {
3454 CursorAffinity::Leading => item.position.x,
3455 CursorAffinity::Trailing => item.position.x + cluster.advance,
3456 };
3457 return Some(LogicalRect {
3458 origin: LogicalPosition {
3459 x: cursor_x,
3460 y: item.position.y,
3461 },
3462 size: LogicalSize {
3463 width: 1.0,
3464 height: line_height,
3465 }, });
3467 }
3468 }
3469 }
3470 None
3471 }
3472
3473 pub fn get_first_cluster_cursor(&self) -> Option<TextCursor> {
3475 for item in &self.items {
3476 if let ShapedItem::Cluster(cluster) = &item.item {
3477 return Some(TextCursor {
3478 cluster_id: cluster.source_cluster_id,
3479 affinity: CursorAffinity::Leading,
3480 });
3481 }
3482 }
3483 None
3484 }
3485
3486 pub fn get_last_cluster_cursor(&self) -> Option<TextCursor> {
3488 for item in self.items.iter().rev() {
3489 if let ShapedItem::Cluster(cluster) = &item.item {
3490 return Some(TextCursor {
3491 cluster_id: cluster.source_cluster_id,
3492 affinity: CursorAffinity::Trailing,
3493 });
3494 }
3495 }
3496 None
3497 }
3498
3499 pub fn move_cursor_left(
3501 &self,
3502 cursor: TextCursor,
3503 debug: &mut Option<Vec<String>>,
3504 ) -> TextCursor {
3505 if let Some(d) = debug {
3506 d.push(format!(
3507 "[Cursor] move_cursor_left: starting at byte {}, affinity {:?}",
3508 cursor.cluster_id.start_byte_in_run, cursor.affinity
3509 ));
3510 }
3511
3512 let current_item_pos = self.items.iter().position(|i| {
3514 i.item
3515 .as_cluster()
3516 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3517 });
3518
3519 let Some(current_pos) = current_item_pos else {
3520 if let Some(d) = debug {
3521 d.push(format!(
3522 "[Cursor] move_cursor_left: cursor not found, staying at byte {}",
3523 cursor.cluster_id.start_byte_in_run
3524 ));
3525 }
3526 return cursor;
3527 };
3528
3529 if cursor.affinity == CursorAffinity::Trailing {
3531 if let Some(d) = debug {
3532 d.push(format!(
3533 "[Cursor] move_cursor_left: moving from trailing to leading edge of byte {}",
3534 cursor.cluster_id.start_byte_in_run
3535 ));
3536 }
3537 return TextCursor {
3538 cluster_id: cursor.cluster_id,
3539 affinity: CursorAffinity::Leading,
3540 };
3541 }
3542
3543 let current_line = self.items[current_pos].line_index;
3546
3547 if let Some(d) = debug {
3548 d.push(format!(
3549 "[Cursor] move_cursor_left: at leading edge, current line {}",
3550 current_line
3551 ));
3552 }
3553
3554 for i in (0..current_pos).rev() {
3556 if let Some(cluster) = self.items[i].item.as_cluster() {
3557 if self.items[i].line_index == current_line {
3558 if let Some(d) = debug {
3559 d.push(format!(
3560 "[Cursor] move_cursor_left: found previous cluster on same line, byte \
3561 {}",
3562 cluster.source_cluster_id.start_byte_in_run
3563 ));
3564 }
3565 return TextCursor {
3566 cluster_id: cluster.source_cluster_id,
3567 affinity: CursorAffinity::Trailing,
3568 };
3569 }
3570 }
3571 }
3572
3573 if current_line > 0 {
3575 let prev_line = current_line - 1;
3576 if let Some(d) = debug {
3577 d.push(format!(
3578 "[Cursor] move_cursor_left: trying previous line {}",
3579 prev_line
3580 ));
3581 }
3582 for i in (0..current_pos).rev() {
3583 if let Some(cluster) = self.items[i].item.as_cluster() {
3584 if self.items[i].line_index == prev_line {
3585 if let Some(d) = debug {
3586 d.push(format!(
3587 "[Cursor] move_cursor_left: found cluster on previous line, byte \
3588 {}",
3589 cluster.source_cluster_id.start_byte_in_run
3590 ));
3591 }
3592 return TextCursor {
3593 cluster_id: cluster.source_cluster_id,
3594 affinity: CursorAffinity::Trailing,
3595 };
3596 }
3597 }
3598 }
3599 }
3600
3601 if let Some(d) = debug {
3603 d.push(format!(
3604 "[Cursor] move_cursor_left: at start of text, staying at byte {}",
3605 cursor.cluster_id.start_byte_in_run
3606 ));
3607 }
3608 cursor
3609 }
3610
3611 pub fn move_cursor_right(
3613 &self,
3614 cursor: TextCursor,
3615 debug: &mut Option<Vec<String>>,
3616 ) -> TextCursor {
3617 if let Some(d) = debug {
3618 d.push(format!(
3619 "[Cursor] move_cursor_right: starting at byte {}, affinity {:?}",
3620 cursor.cluster_id.start_byte_in_run, cursor.affinity
3621 ));
3622 }
3623
3624 let current_item_pos = self.items.iter().position(|i| {
3626 i.item
3627 .as_cluster()
3628 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3629 });
3630
3631 let Some(current_pos) = current_item_pos else {
3632 if let Some(d) = debug {
3633 d.push(format!(
3634 "[Cursor] move_cursor_right: cursor not found, staying at byte {}",
3635 cursor.cluster_id.start_byte_in_run
3636 ));
3637 }
3638 return cursor;
3639 };
3640
3641 if cursor.affinity == CursorAffinity::Leading {
3643 if let Some(d) = debug {
3644 d.push(format!(
3645 "[Cursor] move_cursor_right: moving from leading to trailing edge of byte {}",
3646 cursor.cluster_id.start_byte_in_run
3647 ));
3648 }
3649 return TextCursor {
3650 cluster_id: cursor.cluster_id,
3651 affinity: CursorAffinity::Trailing,
3652 };
3653 }
3654
3655 let current_line = self.items[current_pos].line_index;
3657
3658 if let Some(d) = debug {
3659 d.push(format!(
3660 "[Cursor] move_cursor_right: at trailing edge, current line {}",
3661 current_line
3662 ));
3663 }
3664
3665 for i in (current_pos + 1)..self.items.len() {
3667 if let Some(cluster) = self.items[i].item.as_cluster() {
3668 if self.items[i].line_index == current_line {
3669 if let Some(d) = debug {
3670 d.push(format!(
3671 "[Cursor] move_cursor_right: found next cluster on same line, byte {}",
3672 cluster.source_cluster_id.start_byte_in_run
3673 ));
3674 }
3675 return TextCursor {
3676 cluster_id: cluster.source_cluster_id,
3677 affinity: CursorAffinity::Leading,
3678 };
3679 }
3680 }
3681 }
3682
3683 let next_line = current_line + 1;
3685 if let Some(d) = debug {
3686 d.push(format!(
3687 "[Cursor] move_cursor_right: trying next line {}",
3688 next_line
3689 ));
3690 }
3691 for i in (current_pos + 1)..self.items.len() {
3692 if let Some(cluster) = self.items[i].item.as_cluster() {
3693 if self.items[i].line_index == next_line {
3694 if let Some(d) = debug {
3695 d.push(format!(
3696 "[Cursor] move_cursor_right: found cluster on next line, byte {}",
3697 cluster.source_cluster_id.start_byte_in_run
3698 ));
3699 }
3700 return TextCursor {
3701 cluster_id: cluster.source_cluster_id,
3702 affinity: CursorAffinity::Leading,
3703 };
3704 }
3705 }
3706 }
3707
3708 if let Some(d) = debug {
3710 d.push(format!(
3711 "[Cursor] move_cursor_right: at end of text, staying at byte {}",
3712 cursor.cluster_id.start_byte_in_run
3713 ));
3714 }
3715 cursor
3716 }
3717
3718 pub fn move_cursor_up(
3720 &self,
3721 cursor: TextCursor,
3722 goal_x: &mut Option<f32>,
3723 debug: &mut Option<Vec<String>>,
3724 ) -> TextCursor {
3725 if let Some(d) = debug {
3726 d.push(format!(
3727 "[Cursor] move_cursor_up: from byte {} (affinity {:?})",
3728 cursor.cluster_id.start_byte_in_run, cursor.affinity
3729 ));
3730 }
3731
3732 let Some(current_item) = self.items.iter().find(|i| {
3733 i.item
3734 .as_cluster()
3735 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3736 }) else {
3737 if let Some(d) = debug {
3738 d.push(format!(
3739 "[Cursor] move_cursor_up: cursor not found in items, staying at byte {}",
3740 cursor.cluster_id.start_byte_in_run
3741 ));
3742 }
3743 return cursor;
3744 };
3745
3746 if let Some(d) = debug {
3747 d.push(format!(
3748 "[Cursor] move_cursor_up: current line {}, position ({}, {})",
3749 current_item.line_index, current_item.position.x, current_item.position.y
3750 ));
3751 }
3752
3753 let target_line_idx = current_item.line_index.saturating_sub(1);
3754 if current_item.line_index == target_line_idx {
3755 if let Some(d) = debug {
3756 d.push(format!(
3757 "[Cursor] move_cursor_up: already at top line {}, staying put",
3758 current_item.line_index
3759 ));
3760 }
3761 return cursor;
3762 }
3763
3764 let current_x = goal_x.unwrap_or_else(|| {
3765 let x = match cursor.affinity {
3766 CursorAffinity::Leading => current_item.position.x,
3767 CursorAffinity::Trailing => {
3768 current_item.position.x + get_item_measure(¤t_item.item, false)
3769 }
3770 };
3771 *goal_x = Some(x);
3772 x
3773 });
3774
3775 let target_y = self
3777 .items
3778 .iter()
3779 .find(|i| i.line_index == target_line_idx)
3780 .map(|i| i.position.y + (i.item.bounds().height / 2.0))
3781 .unwrap_or(current_item.position.y);
3782
3783 if let Some(d) = debug {
3784 d.push(format!(
3785 "[Cursor] move_cursor_up: target line {}, hittesting at ({}, {})",
3786 target_line_idx, current_x, target_y
3787 ));
3788 }
3789
3790 let result = self
3791 .hittest_cursor(LogicalPosition {
3792 x: current_x,
3793 y: target_y,
3794 })
3795 .unwrap_or(cursor);
3796
3797 if let Some(d) = debug {
3798 d.push(format!(
3799 "[Cursor] move_cursor_up: result byte {} (affinity {:?})",
3800 result.cluster_id.start_byte_in_run, result.affinity
3801 ));
3802 }
3803
3804 result
3805 }
3806
3807 pub fn move_cursor_down(
3809 &self,
3810 cursor: TextCursor,
3811 goal_x: &mut Option<f32>,
3812 debug: &mut Option<Vec<String>>,
3813 ) -> TextCursor {
3814 if let Some(d) = debug {
3815 d.push(format!(
3816 "[Cursor] move_cursor_down: from byte {} (affinity {:?})",
3817 cursor.cluster_id.start_byte_in_run, cursor.affinity
3818 ));
3819 }
3820
3821 let Some(current_item) = self.items.iter().find(|i| {
3822 i.item
3823 .as_cluster()
3824 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3825 }) else {
3826 if let Some(d) = debug {
3827 d.push(format!(
3828 "[Cursor] move_cursor_down: cursor not found in items, staying at byte {}",
3829 cursor.cluster_id.start_byte_in_run
3830 ));
3831 }
3832 return cursor;
3833 };
3834
3835 if let Some(d) = debug {
3836 d.push(format!(
3837 "[Cursor] move_cursor_down: current line {}, position ({}, {})",
3838 current_item.line_index, current_item.position.x, current_item.position.y
3839 ));
3840 }
3841
3842 let max_line = self.items.iter().map(|i| i.line_index).max().unwrap_or(0);
3843 let target_line_idx = (current_item.line_index + 1).min(max_line);
3844 if current_item.line_index == target_line_idx {
3845 if let Some(d) = debug {
3846 d.push(format!(
3847 "[Cursor] move_cursor_down: already at bottom line {}, staying put",
3848 current_item.line_index
3849 ));
3850 }
3851 return cursor;
3852 }
3853
3854 let current_x = goal_x.unwrap_or_else(|| {
3855 let x = match cursor.affinity {
3856 CursorAffinity::Leading => current_item.position.x,
3857 CursorAffinity::Trailing => {
3858 current_item.position.x + get_item_measure(¤t_item.item, false)
3859 }
3860 };
3861 *goal_x = Some(x);
3862 x
3863 });
3864
3865 let target_y = self
3866 .items
3867 .iter()
3868 .find(|i| i.line_index == target_line_idx)
3869 .map(|i| i.position.y + (i.item.bounds().height / 2.0))
3870 .unwrap_or(current_item.position.y);
3871
3872 if let Some(d) = debug {
3873 d.push(format!(
3874 "[Cursor] move_cursor_down: hit testing at ({}, {})",
3875 current_x, target_y
3876 ));
3877 }
3878
3879 let result = self
3880 .hittest_cursor(LogicalPosition {
3881 x: current_x,
3882 y: target_y,
3883 })
3884 .unwrap_or(cursor);
3885
3886 if let Some(d) = debug {
3887 d.push(format!(
3888 "[Cursor] move_cursor_down: result byte {}, affinity {:?}",
3889 result.cluster_id.start_byte_in_run, result.affinity
3890 ));
3891 }
3892
3893 result
3894 }
3895
3896 pub fn move_cursor_to_line_start(
3898 &self,
3899 cursor: TextCursor,
3900 debug: &mut Option<Vec<String>>,
3901 ) -> TextCursor {
3902 if let Some(d) = debug {
3903 d.push(format!(
3904 "[Cursor] move_cursor_to_line_start: starting at byte {}, affinity {:?}",
3905 cursor.cluster_id.start_byte_in_run, cursor.affinity
3906 ));
3907 }
3908
3909 let Some(current_item) = self.items.iter().find(|i| {
3910 i.item
3911 .as_cluster()
3912 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3913 }) else {
3914 if let Some(d) = debug {
3915 d.push(format!(
3916 "[Cursor] move_cursor_to_line_start: cursor not found, staying at byte {}",
3917 cursor.cluster_id.start_byte_in_run
3918 ));
3919 }
3920 return cursor;
3921 };
3922
3923 if let Some(d) = debug {
3924 d.push(format!(
3925 "[Cursor] move_cursor_to_line_start: current line {}, position ({}, {})",
3926 current_item.line_index, current_item.position.x, current_item.position.y
3927 ));
3928 }
3929
3930 let first_item_on_line = self
3931 .items
3932 .iter()
3933 .filter(|i| i.line_index == current_item.line_index)
3934 .min_by(|a, b| {
3935 a.position
3936 .x
3937 .partial_cmp(&b.position.x)
3938 .unwrap_or(Ordering::Equal)
3939 });
3940
3941 if let Some(item) = first_item_on_line {
3942 if let ShapedItem::Cluster(c) = &item.item {
3943 let result = TextCursor {
3944 cluster_id: c.source_cluster_id,
3945 affinity: CursorAffinity::Leading,
3946 };
3947 if let Some(d) = debug {
3948 d.push(format!(
3949 "[Cursor] move_cursor_to_line_start: result byte {}, affinity {:?}",
3950 result.cluster_id.start_byte_in_run, result.affinity
3951 ));
3952 }
3953 return result;
3954 }
3955 }
3956
3957 if let Some(d) = debug {
3958 d.push(format!(
3959 "[Cursor] move_cursor_to_line_start: no first item found, staying at byte {}",
3960 cursor.cluster_id.start_byte_in_run
3961 ));
3962 }
3963 cursor
3964 }
3965
3966 pub fn move_cursor_to_line_end(
3968 &self,
3969 cursor: TextCursor,
3970 debug: &mut Option<Vec<String>>,
3971 ) -> TextCursor {
3972 if let Some(d) = debug {
3973 d.push(format!(
3974 "[Cursor] move_cursor_to_line_end: starting at byte {}, affinity {:?}",
3975 cursor.cluster_id.start_byte_in_run, cursor.affinity
3976 ));
3977 }
3978
3979 let Some(current_item) = self.items.iter().find(|i| {
3980 i.item
3981 .as_cluster()
3982 .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3983 }) else {
3984 if let Some(d) = debug {
3985 d.push(format!(
3986 "[Cursor] move_cursor_to_line_end: cursor not found, staying at byte {}",
3987 cursor.cluster_id.start_byte_in_run
3988 ));
3989 }
3990 return cursor;
3991 };
3992
3993 if let Some(d) = debug {
3994 d.push(format!(
3995 "[Cursor] move_cursor_to_line_end: current line {}, position ({}, {})",
3996 current_item.line_index, current_item.position.x, current_item.position.y
3997 ));
3998 }
3999
4000 let last_item_on_line = self
4001 .items
4002 .iter()
4003 .filter(|i| i.line_index == current_item.line_index)
4004 .max_by(|a, b| {
4005 a.position
4006 .x
4007 .partial_cmp(&b.position.x)
4008 .unwrap_or(Ordering::Equal)
4009 });
4010
4011 if let Some(item) = last_item_on_line {
4012 if let ShapedItem::Cluster(c) = &item.item {
4013 let result = TextCursor {
4014 cluster_id: c.source_cluster_id,
4015 affinity: CursorAffinity::Trailing,
4016 };
4017 if let Some(d) = debug {
4018 d.push(format!(
4019 "[Cursor] move_cursor_to_line_end: result byte {}, affinity {:?}",
4020 result.cluster_id.start_byte_in_run, result.affinity
4021 ));
4022 }
4023 return result;
4024 }
4025 }
4026
4027 if let Some(d) = debug {
4028 d.push(format!(
4029 "[Cursor] move_cursor_to_line_end: no last item found, staying at byte {}",
4030 cursor.cluster_id.start_byte_in_run
4031 ));
4032 }
4033 cursor
4034 }
4035}
4036
4037fn get_baseline_for_item(item: &ShapedItem) -> Option<f32> {
4038 match item {
4039 ShapedItem::CombinedBlock {
4040 baseline_offset, ..
4041 } => Some(*baseline_offset),
4042 ShapedItem::Object {
4043 baseline_offset, ..
4044 } => Some(*baseline_offset),
4045 ShapedItem::Cluster(ref cluster) => {
4047 if let Some(last_glyph) = cluster.glyphs.last() {
4048 Some(
4049 last_glyph
4050 .font_metrics
4051 .baseline_scaled(last_glyph.style.font_size_px),
4052 )
4053 } else {
4054 None
4055 }
4056 }
4057 ShapedItem::Break { source, break_info } => {
4058 None
4060 }
4061 ShapedItem::Tab { source, bounds } => {
4062 None
4064 }
4065 }
4066}
4067
4068#[derive(Debug, Clone, Default)]
4070pub struct OverflowInfo {
4071 pub overflow_items: Vec<ShapedItem>,
4073 pub unclipped_bounds: Rect,
4076}
4077
4078impl OverflowInfo {
4079 pub fn has_overflow(&self) -> bool {
4080 !self.overflow_items.is_empty()
4081 }
4082}
4083
4084#[derive(Debug, Clone)]
4086pub struct UnifiedLine {
4087 pub items: Vec<ShapedItem>,
4088 pub cross_axis_position: f32,
4090 pub constraints: LineConstraints,
4092 pub is_last: bool,
4093}
4094
4095pub type CacheId = u64;
4098
4099#[derive(Debug, Clone)]
4101pub struct LayoutFragment {
4102 pub id: String,
4104 pub constraints: UnifiedConstraints,
4106}
4107
4108#[derive(Debug, Clone)]
4110pub struct FlowLayout {
4111 pub fragment_layouts: HashMap<String, Arc<UnifiedLayout>>,
4113 pub remaining_items: Vec<ShapedItem>,
4116}
4117
4118pub struct LayoutCache {
4119 logical_items: HashMap<CacheId, Arc<Vec<LogicalItem>>>,
4121 visual_items: HashMap<CacheId, Arc<Vec<VisualItem>>>,
4123 shaped_items: HashMap<CacheId, Arc<Vec<ShapedItem>>>,
4125 layouts: HashMap<CacheId, Arc<UnifiedLayout>>,
4127}
4128
4129impl LayoutCache {
4130 pub fn new() -> Self {
4131 Self {
4132 logical_items: HashMap::new(),
4133 visual_items: HashMap::new(),
4134 shaped_items: HashMap::new(),
4135 layouts: HashMap::new(),
4136 }
4137 }
4138
4139 pub fn get_layout(&self, cache_id: &CacheId) -> Option<&Arc<UnifiedLayout>> {
4141 self.layouts.get(cache_id)
4142 }
4143
4144 pub fn get_all_layout_ids(&self) -> Vec<CacheId> {
4146 self.layouts.keys().copied().collect()
4147 }
4148
4149 pub fn use_old_layout(
4164 old_constraints: &UnifiedConstraints,
4165 new_constraints: &UnifiedConstraints,
4166 old_content: &[InlineContent],
4167 new_content: &[InlineContent],
4168 ) -> bool {
4169 if old_constraints != new_constraints {
4171 return false;
4172 }
4173
4174 if old_content.len() != new_content.len() {
4176 return false;
4177 }
4178
4179 for (old, new) in old_content.iter().zip(new_content.iter()) {
4181 if !Self::inline_content_layout_eq(old, new) {
4182 return false;
4183 }
4184 }
4185
4186 true
4187 }
4188
4189 fn inline_content_layout_eq(old: &InlineContent, new: &InlineContent) -> bool {
4193 use InlineContent::*;
4194 match (old, new) {
4195 (Text(old_run), Text(new_run)) => {
4196 old_run.text == new_run.text
4198 && old_run.style.layout_eq(&new_run.style)
4199 }
4200 (Image(old_img), Image(new_img)) => {
4201 old_img.intrinsic_size == new_img.intrinsic_size
4203 && old_img.display_size == new_img.display_size
4204 && old_img.baseline_offset == new_img.baseline_offset
4205 && old_img.alignment == new_img.alignment
4206 }
4207 (Space(old_sp), Space(new_sp)) => old_sp == new_sp,
4208 (LineBreak(old_br), LineBreak(new_br)) => old_br == new_br,
4209 (Tab, Tab) => true,
4210 (Marker { run: old_run, position_outside: old_pos },
4211 Marker { run: new_run, position_outside: new_pos }) => {
4212 old_pos == new_pos
4213 && old_run.text == new_run.text
4214 && old_run.style.layout_eq(&new_run.style)
4215 }
4216 (Shape(old_shape), Shape(new_shape)) => {
4217 old_shape.shape_def == new_shape.shape_def
4219 && old_shape.baseline_offset == new_shape.baseline_offset
4220 }
4221 (Ruby { base: old_base, text: old_text, style: old_style },
4222 Ruby { base: new_base, text: new_text, style: new_style }) => {
4223 old_style.layout_eq(new_style)
4224 && old_base.len() == new_base.len()
4225 && old_text.len() == new_text.len()
4226 && old_base.iter().zip(new_base.iter())
4227 .all(|(o, n)| Self::inline_content_layout_eq(o, n))
4228 && old_text.iter().zip(new_text.iter())
4229 .all(|(o, n)| Self::inline_content_layout_eq(o, n))
4230 }
4231 _ => false,
4233 }
4234 }
4235}
4236
4237impl Default for LayoutCache {
4238 fn default() -> Self {
4239 Self::new()
4240 }
4241}
4242
4243#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4245pub struct LogicalItemsKey<'a> {
4246 pub inline_content_hash: u64, pub default_font_size: u32, pub _marker: std::marker::PhantomData<&'a ()>,
4250}
4251
4252#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4254pub struct VisualItemsKey {
4255 pub logical_items_id: CacheId,
4256 pub base_direction: BidiDirection,
4257}
4258
4259#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4261pub struct ShapedItemsKey {
4262 pub visual_items_id: CacheId,
4263 pub style_hash: u64, }
4265
4266impl ShapedItemsKey {
4267 pub fn new(visual_items_id: CacheId, visual_items: &[VisualItem]) -> Self {
4268 let style_hash = {
4269 let mut hasher = DefaultHasher::new();
4270 for item in visual_items.iter() {
4271 match &item.logical_source {
4273 LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
4274 style.as_ref().hash(&mut hasher);
4275 }
4276 _ => {}
4277 }
4278 }
4279 hasher.finish()
4280 };
4281
4282 Self {
4283 visual_items_id,
4284 style_hash,
4285 }
4286 }
4287}
4288
4289#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4291pub struct LayoutKey {
4292 pub shaped_items_id: CacheId,
4293 pub constraints: UnifiedConstraints,
4294}
4295
4296fn calculate_id<T: Hash>(item: &T) -> CacheId {
4298 let mut hasher = DefaultHasher::new();
4299 item.hash(&mut hasher);
4300 hasher.finish()
4301}
4302
4303impl LayoutCache {
4306 pub fn layout_flow<T: ParsedFontTrait>(
4364 &mut self,
4365 content: &[InlineContent],
4366 style_overrides: &[StyleOverride],
4367 flow_chain: &[LayoutFragment],
4368 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
4369 fc_cache: &FcFontCache,
4370 loaded_fonts: &LoadedFonts<T>,
4371 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4372 ) -> Result<FlowLayout, LayoutError> {
4373 let logical_items_id = calculate_id(&content);
4379 let logical_items = self
4380 .logical_items
4381 .entry(logical_items_id)
4382 .or_insert_with(|| {
4383 Arc::new(create_logical_items(
4384 content,
4385 style_overrides,
4386 debug_messages,
4387 ))
4388 })
4389 .clone();
4390
4391 let default_constraints = UnifiedConstraints::default();
4394 let first_constraints = flow_chain
4395 .first()
4396 .map(|f| &f.constraints)
4397 .unwrap_or(&default_constraints);
4398
4399 let base_direction = first_constraints.direction.unwrap_or(BidiDirection::Ltr);
4406 let visual_key = VisualItemsKey {
4407 logical_items_id,
4408 base_direction,
4409 };
4410 let visual_items_id = calculate_id(&visual_key);
4411 let visual_items = self
4412 .visual_items
4413 .entry(visual_items_id)
4414 .or_insert_with(|| {
4415 Arc::new(
4416 reorder_logical_items(&logical_items, base_direction, debug_messages).unwrap(),
4417 )
4418 })
4419 .clone();
4420
4421 let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
4423 let shaped_items_id = calculate_id(&shaped_key);
4424 let shaped_items = match self.shaped_items.get(&shaped_items_id) {
4425 Some(cached) => cached.clone(),
4426 None => {
4427 let items = Arc::new(shape_visual_items(
4428 &visual_items,
4429 font_chain_cache,
4430 fc_cache,
4431 loaded_fonts,
4432 debug_messages,
4433 )?);
4434 self.shaped_items.insert(shaped_items_id, items.clone());
4435 items
4436 }
4437 };
4438
4439 let oriented_items = apply_text_orientation(shaped_items, first_constraints)?;
4446
4447 let mut fragment_layouts = HashMap::new();
4450 let mut cursor = BreakCursor::new(&oriented_items);
4452
4453 for fragment in flow_chain {
4454 let fragment_layout = perform_fragment_layout(
4456 &mut cursor,
4457 &logical_items,
4458 &fragment.constraints,
4459 debug_messages,
4460 loaded_fonts,
4461 )?;
4462
4463 fragment_layouts.insert(fragment.id.clone(), Arc::new(fragment_layout));
4464 if cursor.is_done() {
4465 break; }
4467 }
4468
4469 Ok(FlowLayout {
4470 fragment_layouts,
4471 remaining_items: cursor.drain_remaining(),
4472 })
4473 }
4474}
4475
4476pub fn create_logical_items(
4478 content: &[InlineContent],
4479 style_overrides: &[StyleOverride],
4480 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4481) -> Vec<LogicalItem> {
4482 if let Some(msgs) = debug_messages {
4483 msgs.push(LayoutDebugMessage::info(
4484 "\n--- Entering create_logical_items (Refactored) ---".to_string(),
4485 ));
4486 msgs.push(LayoutDebugMessage::info(format!(
4487 "Input content length: {}",
4488 content.len()
4489 )));
4490 msgs.push(LayoutDebugMessage::info(format!(
4491 "Input overrides length: {}",
4492 style_overrides.len()
4493 )));
4494 }
4495
4496 let mut items = Vec::new();
4497 let mut style_cache: HashMap<u64, Arc<StyleProperties>> = HashMap::new();
4498
4499 let mut run_overrides: HashMap<u32, HashMap<u32, &PartialStyleProperties>> = HashMap::new();
4501 for override_item in style_overrides {
4502 run_overrides
4503 .entry(override_item.target.run_index)
4504 .or_default()
4505 .insert(override_item.target.item_index, &override_item.style);
4506 }
4507
4508 for (run_idx, inline_item) in content.iter().enumerate() {
4509 if let Some(msgs) = debug_messages {
4510 msgs.push(LayoutDebugMessage::info(format!(
4511 "Processing content run #{}",
4512 run_idx
4513 )));
4514 }
4515
4516 let marker_position_outside = match inline_item {
4518 InlineContent::Marker {
4519 position_outside, ..
4520 } => Some(*position_outside),
4521 _ => None,
4522 };
4523
4524 match inline_item {
4525 InlineContent::Text(run) | InlineContent::Marker { run, .. } => {
4526 let text = &run.text;
4527 if text.is_empty() {
4528 if let Some(msgs) = debug_messages {
4529 msgs.push(LayoutDebugMessage::info(
4530 " Run is empty, skipping.".to_string(),
4531 ));
4532 }
4533 continue;
4534 }
4535 if let Some(msgs) = debug_messages {
4536 msgs.push(LayoutDebugMessage::info(format!(" Run text: '{}'", text)));
4537 }
4538
4539 let current_run_overrides = run_overrides.get(&(run_idx as u32));
4540 let mut boundaries = BTreeSet::new();
4541 boundaries.insert(0);
4542 boundaries.insert(text.len());
4543
4544 let mut scan_cursor = 0;
4546 while scan_cursor < text.len() {
4547 let style_at_cursor = if let Some(partial) =
4548 current_run_overrides.and_then(|o| o.get(&(scan_cursor as u32)))
4549 {
4550 run.style.apply_override(partial)
4552 } else {
4553 (*run.style).clone()
4554 };
4555
4556 let current_char = text[scan_cursor..].chars().next().unwrap();
4557
4558 if let Some(TextCombineUpright::Digits(max_digits)) =
4560 style_at_cursor.text_combine_upright
4561 {
4562 if max_digits > 0 && current_char.is_ascii_digit() {
4563 let digit_chunk: String = text[scan_cursor..]
4564 .chars()
4565 .take(max_digits as usize)
4566 .take_while(|c| c.is_ascii_digit())
4567 .collect();
4568
4569 let end_of_chunk = scan_cursor + digit_chunk.len();
4570 boundaries.insert(scan_cursor);
4571 boundaries.insert(end_of_chunk);
4572 scan_cursor = end_of_chunk; continue;
4574 }
4575 }
4576
4577 if current_run_overrides
4580 .and_then(|o| o.get(&(scan_cursor as u32)))
4581 .is_some()
4582 {
4583 let grapheme_len = text[scan_cursor..]
4584 .graphemes(true)
4585 .next()
4586 .unwrap_or("")
4587 .len();
4588 boundaries.insert(scan_cursor);
4589 boundaries.insert(scan_cursor + grapheme_len);
4590 scan_cursor += grapheme_len;
4591 continue;
4592 }
4593
4594 scan_cursor += current_char.len_utf8();
4597 }
4598
4599 if let Some(msgs) = debug_messages {
4600 msgs.push(LayoutDebugMessage::info(format!(
4601 " Boundaries: {:?}",
4602 boundaries
4603 )));
4604 }
4605
4606 for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
4608 let (start, end) = (*start, *end);
4609 if start >= end {
4610 continue;
4611 }
4612
4613 let text_slice = &text[start..end];
4614 if let Some(msgs) = debug_messages {
4615 msgs.push(LayoutDebugMessage::info(format!(
4616 " Processing chunk from {} to {}: '{}'",
4617 start, end, text_slice
4618 )));
4619 }
4620
4621 let style_to_use = if let Some(partial_style) =
4622 current_run_overrides.and_then(|o| o.get(&(start as u32)))
4623 {
4624 if let Some(msgs) = debug_messages {
4625 msgs.push(LayoutDebugMessage::info(format!(
4626 " -> Applying override at byte {}",
4627 start
4628 )));
4629 }
4630 let mut hasher = DefaultHasher::new();
4631 Arc::as_ptr(&run.style).hash(&mut hasher);
4632 partial_style.hash(&mut hasher);
4633 style_cache
4634 .entry(hasher.finish())
4635 .or_insert_with(|| Arc::new(run.style.apply_override(partial_style)))
4636 .clone()
4637 } else {
4638 run.style.clone()
4639 };
4640
4641 let is_combinable_chunk = if let Some(TextCombineUpright::Digits(max_digits)) =
4642 &style_to_use.text_combine_upright
4643 {
4644 *max_digits > 0
4645 && !text_slice.is_empty()
4646 && text_slice.chars().all(|c| c.is_ascii_digit())
4647 && text_slice.chars().count() <= *max_digits as usize
4648 } else {
4649 false
4650 };
4651
4652 if is_combinable_chunk {
4653 items.push(LogicalItem::CombinedText {
4654 source: ContentIndex {
4655 run_index: run_idx as u32,
4656 item_index: start as u32,
4657 },
4658 text: text_slice.to_string(),
4659 style: style_to_use,
4660 });
4661 } else {
4662 items.push(LogicalItem::Text {
4663 source: ContentIndex {
4664 run_index: run_idx as u32,
4665 item_index: start as u32,
4666 },
4667 text: text_slice.to_string(),
4668 style: style_to_use,
4669 marker_position_outside,
4670 source_node_id: run.source_node_id,
4671 });
4672 }
4673 }
4674 }
4675 InlineContent::LineBreak(break_info) => {
4677 if let Some(msgs) = debug_messages {
4678 msgs.push(LayoutDebugMessage::info(format!(
4679 " LineBreak: {:?}",
4680 break_info
4681 )));
4682 }
4683 items.push(LogicalItem::Break {
4684 source: ContentIndex {
4685 run_index: run_idx as u32,
4686 item_index: 0,
4687 },
4688 break_info: break_info.clone(),
4689 });
4690 }
4691 _ => {
4693 if let Some(msgs) = debug_messages {
4694 msgs.push(LayoutDebugMessage::info(
4695 " Run is not text, creating generic LogicalItem.".to_string(),
4696 ));
4697 }
4698 items.push(LogicalItem::Object {
4699 source: ContentIndex {
4700 run_index: run_idx as u32,
4701 item_index: 0,
4702 },
4703 content: inline_item.clone(),
4704 });
4705 }
4706 }
4707 }
4708 if let Some(msgs) = debug_messages {
4709 msgs.push(LayoutDebugMessage::info(format!(
4710 "--- Exiting create_logical_items, created {} items ---",
4711 items.len()
4712 )));
4713 }
4714 items
4715}
4716
4717pub fn get_base_direction_from_logical(logical_items: &[LogicalItem]) -> BidiDirection {
4720 let first_strong = logical_items.iter().find_map(|item| {
4721 if let LogicalItem::Text { text, .. } = item {
4722 Some(unicode_bidi::get_base_direction(text.as_str()))
4723 } else {
4724 None
4725 }
4726 });
4727
4728 match first_strong {
4729 Some(unicode_bidi::Direction::Rtl) => BidiDirection::Rtl,
4730 _ => BidiDirection::Ltr,
4731 }
4732}
4733
4734pub fn reorder_logical_items(
4735 logical_items: &[LogicalItem],
4736 base_direction: BidiDirection,
4737 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4738) -> Result<Vec<VisualItem>, LayoutError> {
4739 if let Some(msgs) = debug_messages {
4740 msgs.push(LayoutDebugMessage::info(
4741 "\n--- Entering reorder_logical_items ---".to_string(),
4742 ));
4743 msgs.push(LayoutDebugMessage::info(format!(
4744 "Input logical items count: {}",
4745 logical_items.len()
4746 )));
4747 msgs.push(LayoutDebugMessage::info(format!(
4748 "Base direction: {:?}",
4749 base_direction
4750 )));
4751 }
4752
4753 let mut bidi_str = String::new();
4754 let mut item_map = Vec::new();
4755 for (idx, item) in logical_items.iter().enumerate() {
4756 let text = match item {
4757 LogicalItem::Text { text, .. } => text.as_str(),
4758 LogicalItem::CombinedText { text, .. } => text.as_str(),
4759 _ => "\u{FFFC}",
4760 };
4761 let start_byte = bidi_str.len();
4762 bidi_str.push_str(text);
4763 for _ in start_byte..bidi_str.len() {
4764 item_map.push(idx);
4765 }
4766 }
4767
4768 if bidi_str.is_empty() {
4769 if let Some(msgs) = debug_messages {
4770 msgs.push(LayoutDebugMessage::info(
4771 "Bidi string is empty, returning.".to_string(),
4772 ));
4773 }
4774 return Ok(Vec::new());
4775 }
4776 if let Some(msgs) = debug_messages {
4777 msgs.push(LayoutDebugMessage::info(format!(
4778 "Constructed bidi string: '{}'",
4779 bidi_str
4780 )));
4781 }
4782
4783 let bidi_level = if base_direction == BidiDirection::Rtl {
4784 Some(Level::rtl())
4785 } else {
4786 Some(Level::ltr())
4787 };
4788 let bidi_info = BidiInfo::new(&bidi_str, bidi_level);
4789 let para = &bidi_info.paragraphs[0];
4790 let (levels, visual_runs) = bidi_info.visual_runs(para, para.range.clone());
4791
4792 if let Some(msgs) = debug_messages {
4793 msgs.push(LayoutDebugMessage::info(
4794 "Bidi visual runs generated:".to_string(),
4795 ));
4796 for (i, run_range) in visual_runs.iter().enumerate() {
4797 let level = levels[run_range.start].number();
4798 let slice = &bidi_str[run_range.start..run_range.end];
4799 msgs.push(LayoutDebugMessage::info(format!(
4800 " Run {}: range={:?}, level={}, text='{}'",
4801 i, run_range, level, slice
4802 )));
4803 }
4804 }
4805
4806 let mut visual_items = Vec::new();
4807 for run_range in visual_runs {
4808 let bidi_level = BidiLevel::new(levels[run_range.start].number());
4809 let mut sub_run_start = run_range.start;
4810
4811 for i in (run_range.start + 1)..run_range.end {
4812 if item_map[i] != item_map[sub_run_start] {
4813 let logical_idx = item_map[sub_run_start];
4814 let logical_item = &logical_items[logical_idx];
4815 let text_slice = &bidi_str[sub_run_start..i];
4816 visual_items.push(VisualItem {
4817 logical_source: logical_item.clone(),
4818 bidi_level,
4819 script: crate::text3::script::detect_script(text_slice)
4820 .unwrap_or(Script::Latin),
4821 text: text_slice.to_string(),
4822 });
4823 sub_run_start = i;
4824 }
4825 }
4826
4827 let logical_idx = item_map[sub_run_start];
4828 let logical_item = &logical_items[logical_idx];
4829 let text_slice = &bidi_str[sub_run_start..run_range.end];
4830 visual_items.push(VisualItem {
4831 logical_source: logical_item.clone(),
4832 bidi_level,
4833 script: crate::text3::script::detect_script(text_slice).unwrap_or(Script::Latin),
4834 text: text_slice.to_string(),
4835 });
4836 }
4837
4838 if let Some(msgs) = debug_messages {
4839 msgs.push(LayoutDebugMessage::info(
4840 "Final visual items produced:".to_string(),
4841 ));
4842 for (i, item) in visual_items.iter().enumerate() {
4843 msgs.push(LayoutDebugMessage::info(format!(
4844 " Item {}: level={}, text='{}'",
4845 i,
4846 item.bidi_level.level(),
4847 item.text
4848 )));
4849 }
4850 msgs.push(LayoutDebugMessage::info(
4851 "--- Exiting reorder_logical_items ---".to_string(),
4852 ));
4853 }
4854 Ok(visual_items)
4855}
4856
4857pub fn shape_visual_items<T: ParsedFontTrait>(
4864 visual_items: &[VisualItem],
4865 font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
4866 fc_cache: &FcFontCache,
4867 loaded_fonts: &LoadedFonts<T>,
4868 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4869) -> Result<Vec<ShapedItem>, LayoutError> {
4870 let mut shaped = Vec::new();
4871
4872 for item in visual_items {
4873 match &item.logical_source {
4874 LogicalItem::Text {
4875 style,
4876 source,
4877 marker_position_outside,
4878 source_node_id,
4879 ..
4880 } => {
4881 let direction = if item.bidi_level.is_rtl() {
4882 BidiDirection::Rtl
4883 } else {
4884 BidiDirection::Ltr
4885 };
4886
4887 let language = script_to_language(item.script, &item.text);
4888
4889 let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
4891 FontStack::Ref(font_ref) => {
4892 if let Some(msgs) = debug_messages {
4894 msgs.push(LayoutDebugMessage::info(format!(
4895 "[TextLayout] Using direct FontRef for text: '{}'",
4896 item.text.chars().take(30).collect::<String>()
4897 )));
4898 }
4899 shape_text_correctly(
4900 &item.text,
4901 item.script,
4902 language,
4903 direction,
4904 font_ref,
4905 style,
4906 *source,
4907 *source_node_id,
4908 )
4909 }
4910 FontStack::Stack(selectors) => {
4911 let cache_key = FontChainKey::from_selectors(selectors);
4913
4914 let font_chain = match font_chain_cache.get(&cache_key) {
4916 Some(chain) => chain,
4917 None => {
4918 if let Some(msgs) = debug_messages {
4919 msgs.push(LayoutDebugMessage::warning(format!(
4920 "[TextLayout] Font chain not pre-resolved for {:?} - text will \
4921 not be rendered",
4922 cache_key.font_families
4923 )));
4924 }
4925 continue;
4926 }
4927 };
4928
4929 let first_char = item.text.chars().next().unwrap_or('A');
4931 let font_id = match font_chain.resolve_char(fc_cache, first_char) {
4932 Some((id, _css_source)) => id,
4933 None => {
4934 if let Some(msgs) = debug_messages {
4935 msgs.push(LayoutDebugMessage::warning(format!(
4936 "[TextLayout] No font in chain can render character '{}' \
4937 (U+{:04X})",
4938 first_char, first_char as u32
4939 )));
4940 }
4941 continue;
4942 }
4943 };
4944
4945 match loaded_fonts.get(&font_id) {
4947 Some(font) => {
4948 shape_text_correctly(
4949 &item.text,
4950 item.script,
4951 language,
4952 direction,
4953 font,
4954 style,
4955 *source,
4956 *source_node_id,
4957 )
4958 }
4959 None => {
4960 if let Some(msgs) = debug_messages {
4961 let truncated_text = item.text.chars().take(50).collect::<String>();
4962 let display_text = if item.text.chars().count() > 50 {
4963 format!("{}...", truncated_text)
4964 } else {
4965 truncated_text
4966 };
4967
4968 msgs.push(LayoutDebugMessage::warning(format!(
4969 "[TextLayout] Font {:?} not pre-loaded for text: '{}'",
4970 font_id, display_text
4971 )));
4972 }
4973 continue;
4974 }
4975 }
4976 }
4977 };
4978
4979 let mut shaped_clusters = shaped_clusters_result?;
4980
4981 if let Some(is_outside) = marker_position_outside {
4983 for cluster in &mut shaped_clusters {
4984 cluster.marker_position_outside = Some(*is_outside);
4985 }
4986 }
4987
4988 shaped.extend(shaped_clusters.into_iter().map(ShapedItem::Cluster));
4989 }
4990 LogicalItem::Tab { source, style } => {
4991 let space_advance = style.font_size_px * 0.33;
4995 let tab_width = style.tab_size * space_advance;
4996 shaped.push(ShapedItem::Tab {
4997 source: *source,
4998 bounds: Rect {
4999 x: 0.0,
5000 y: 0.0,
5001 width: tab_width,
5002 height: 0.0,
5003 },
5004 });
5005 }
5006 LogicalItem::Ruby {
5007 source,
5008 base_text,
5009 ruby_text,
5010 style,
5011 } => {
5012 let placeholder_width = base_text.chars().count() as f32 * style.font_size_px * 0.6;
5022 shaped.push(ShapedItem::Object {
5023 source: *source,
5024 bounds: Rect {
5025 x: 0.0,
5026 y: 0.0,
5027 width: placeholder_width,
5028 height: style.line_height * 1.5,
5029 },
5030 baseline_offset: 0.0,
5031 content: InlineContent::Text(StyledRun {
5032 text: base_text.clone(),
5033 style: style.clone(),
5034 logical_start_byte: 0,
5035 source_node_id: None, }),
5037 });
5038 }
5039 LogicalItem::CombinedText {
5040 style,
5041 source,
5042 text,
5043 } => {
5044 let language = script_to_language(item.script, &item.text);
5045
5046 let glyphs: Vec<Glyph> = match &style.font_stack {
5048 FontStack::Ref(font_ref) => {
5049 if let Some(msgs) = debug_messages {
5051 msgs.push(LayoutDebugMessage::info(format!(
5052 "[TextLayout] Using direct FontRef for CombinedText: '{}'",
5053 text.chars().take(30).collect::<String>()
5054 )));
5055 }
5056 font_ref.shape_text(
5057 text,
5058 item.script,
5059 language,
5060 BidiDirection::Ltr,
5061 style.as_ref(),
5062 )?
5063 }
5064 FontStack::Stack(selectors) => {
5065 let cache_key = FontChainKey::from_selectors(selectors);
5067
5068 let font_chain = match font_chain_cache.get(&cache_key) {
5069 Some(chain) => chain,
5070 None => {
5071 if let Some(msgs) = debug_messages {
5072 msgs.push(LayoutDebugMessage::warning(format!(
5073 "[TextLayout] Font chain not pre-resolved for CombinedText {:?}",
5074 cache_key.font_families
5075 )));
5076 }
5077 continue;
5078 }
5079 };
5080
5081 let first_char = text.chars().next().unwrap_or('A');
5082 let font_id = match font_chain.resolve_char(fc_cache, first_char) {
5083 Some((id, _)) => id,
5084 None => {
5085 if let Some(msgs) = debug_messages {
5086 msgs.push(LayoutDebugMessage::warning(format!(
5087 "[TextLayout] No font for CombinedText char '{}'",
5088 first_char
5089 )));
5090 }
5091 continue;
5092 }
5093 };
5094
5095 match loaded_fonts.get(&font_id) {
5096 Some(font) => {
5097 font.shape_text(
5098 text,
5099 item.script,
5100 language,
5101 BidiDirection::Ltr,
5102 style.as_ref(),
5103 )?
5104 }
5105 None => {
5106 if let Some(msgs) = debug_messages {
5107 msgs.push(LayoutDebugMessage::warning(format!(
5108 "[TextLayout] Font {:?} not pre-loaded for CombinedText",
5109 font_id
5110 )));
5111 }
5112 continue;
5113 }
5114 }
5115 }
5116 };
5117
5118 let shaped_glyphs = glyphs
5119 .into_iter()
5120 .map(|g| ShapedGlyph {
5121 kind: GlyphKind::Character,
5122 glyph_id: g.glyph_id,
5123 script: g.script,
5124 font_hash: g.font_hash,
5125 font_metrics: g.font_metrics,
5126 style: g.style,
5127 cluster_offset: 0,
5128 advance: g.advance,
5129 kerning: g.kerning,
5130 offset: g.offset,
5131 vertical_advance: g.vertical_advance,
5132 vertical_offset: g.vertical_bearing,
5133 })
5134 .collect::<Vec<_>>();
5135
5136 let total_width: f32 = shaped_glyphs.iter().map(|g| g.advance + g.kerning).sum();
5137 let bounds = Rect {
5138 x: 0.0,
5139 y: 0.0,
5140 width: total_width,
5141 height: style.line_height,
5142 };
5143
5144 shaped.push(ShapedItem::CombinedBlock {
5145 source: *source,
5146 glyphs: shaped_glyphs,
5147 bounds,
5148 baseline_offset: 0.0,
5149 });
5150 }
5151 LogicalItem::Object {
5152 content, source, ..
5153 } => {
5154 let (bounds, baseline) = measure_inline_object(content)?;
5155 shaped.push(ShapedItem::Object {
5156 source: *source,
5157 bounds,
5158 baseline_offset: baseline,
5159 content: content.clone(),
5160 });
5161 }
5162 LogicalItem::Break { source, break_info } => {
5163 shaped.push(ShapedItem::Break {
5164 source: *source,
5165 break_info: break_info.clone(),
5166 });
5167 }
5168 }
5169 }
5170 Ok(shaped)
5171}
5172
5173fn is_hanging_punctuation(item: &ShapedItem) -> bool {
5175 if let ShapedItem::Cluster(c) = item {
5176 if c.glyphs.len() == 1 {
5177 match c.text.as_str() {
5178 "." | "," | ":" | ";" => true,
5179 _ => false,
5180 }
5181 } else {
5182 false
5183 }
5184 } else {
5185 false
5186 }
5187}
5188
5189fn shape_text_correctly<T: ParsedFontTrait>(
5190 text: &str,
5191 script: Script,
5192 language: crate::text3::script::Language,
5193 direction: BidiDirection,
5194 font: &T, style: &Arc<StyleProperties>,
5196 source_index: ContentIndex,
5197 source_node_id: Option<NodeId>,
5198) -> Result<Vec<ShapedCluster>, LayoutError> {
5199 let glyphs = font.shape_text(text, script, language, direction, style.as_ref())?;
5200
5201 if glyphs.is_empty() {
5202 return Ok(Vec::new());
5203 }
5204
5205 let mut clusters = Vec::new();
5206
5207 let mut current_cluster_glyphs = Vec::new();
5209 let mut cluster_id = glyphs[0].cluster;
5210 let mut cluster_start_byte_in_text = glyphs[0].logical_byte_index;
5211
5212 for glyph in glyphs {
5213 if glyph.cluster != cluster_id {
5214 let advance = current_cluster_glyphs
5216 .iter()
5217 .map(|g: &Glyph| g.advance)
5218 .sum();
5219
5220 let (start, end) = if cluster_start_byte_in_text <= glyph.logical_byte_index {
5223 (cluster_start_byte_in_text, glyph.logical_byte_index)
5224 } else {
5225 (glyph.logical_byte_index, cluster_start_byte_in_text)
5226 };
5227 let cluster_text = text.get(start..end).unwrap_or("");
5228
5229 clusters.push(ShapedCluster {
5230 text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
5232 source_run: source_index.run_index,
5233 start_byte_in_run: cluster_id,
5234 },
5235 source_content_index: source_index,
5236 source_node_id,
5237 glyphs: current_cluster_glyphs
5238 .iter()
5239 .map(|g| {
5240 let source_char = text
5241 .get(g.logical_byte_index..)
5242 .and_then(|s| s.chars().next())
5243 .unwrap_or('\u{FFFD}');
5244 let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
5246 (g.logical_byte_index - cluster_start_byte_in_text) as u32
5247 } else {
5248 0
5249 };
5250 ShapedGlyph {
5251 kind: if g.glyph_id == 0 {
5252 GlyphKind::NotDef
5253 } else {
5254 GlyphKind::Character
5255 },
5256 glyph_id: g.glyph_id,
5257 script: g.script,
5258 font_hash: g.font_hash,
5259 font_metrics: g.font_metrics.clone(),
5260 style: g.style.clone(),
5261 cluster_offset,
5262 advance: g.advance,
5263 kerning: g.kerning,
5264 vertical_advance: g.vertical_advance,
5265 vertical_offset: g.vertical_bearing,
5266 offset: g.offset,
5267 }
5268 })
5269 .collect(),
5270 advance,
5271 direction,
5272 style: style.clone(),
5273 marker_position_outside: None,
5274 });
5275 current_cluster_glyphs.clear();
5276 cluster_id = glyph.cluster;
5277 cluster_start_byte_in_text = glyph.logical_byte_index;
5278 }
5279 current_cluster_glyphs.push(glyph);
5280 }
5281
5282 if !current_cluster_glyphs.is_empty() {
5284 let advance = current_cluster_glyphs
5285 .iter()
5286 .map(|g: &Glyph| g.advance)
5287 .sum();
5288 let cluster_text = text.get(cluster_start_byte_in_text..).unwrap_or("");
5289 clusters.push(ShapedCluster {
5290 text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
5292 source_run: source_index.run_index,
5293 start_byte_in_run: cluster_id,
5294 },
5295 source_content_index: source_index,
5296 source_node_id,
5297 glyphs: current_cluster_glyphs
5298 .iter()
5299 .map(|g| {
5300 let source_char = text
5301 .get(g.logical_byte_index..)
5302 .and_then(|s| s.chars().next())
5303 .unwrap_or('\u{FFFD}');
5304 let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
5306 (g.logical_byte_index - cluster_start_byte_in_text) as u32
5307 } else {
5308 0
5309 };
5310 ShapedGlyph {
5311 kind: if g.glyph_id == 0 {
5312 GlyphKind::NotDef
5313 } else {
5314 GlyphKind::Character
5315 },
5316 glyph_id: g.glyph_id,
5317 font_hash: g.font_hash,
5318 font_metrics: g.font_metrics.clone(),
5319 style: g.style.clone(),
5320 script: g.script,
5321 vertical_advance: g.vertical_advance,
5322 vertical_offset: g.vertical_bearing,
5323 cluster_offset,
5324 advance: g.advance,
5325 kerning: g.kerning,
5326 offset: g.offset,
5327 }
5328 })
5329 .collect(),
5330 advance,
5331 direction,
5332 style: style.clone(),
5333 marker_position_outside: None,
5334 });
5335 }
5336
5337 Ok(clusters)
5338}
5339
5340fn measure_inline_object(item: &InlineContent) -> Result<(Rect, f32), LayoutError> {
5342 match item {
5343 InlineContent::Image(img) => {
5344 let size = img.display_size.unwrap_or(img.intrinsic_size);
5345 Ok((
5346 Rect {
5347 x: 0.0,
5348 y: 0.0,
5349 width: size.width,
5350 height: size.height,
5351 },
5352 img.baseline_offset,
5353 ))
5354 }
5355 InlineContent::Shape(shape) => Ok({
5356 let size = shape.shape_def.get_size();
5357 (
5358 Rect {
5359 x: 0.0,
5360 y: 0.0,
5361 width: size.width,
5362 height: size.height,
5363 },
5364 shape.baseline_offset,
5365 )
5366 }),
5367 InlineContent::Space(space) => Ok((
5368 Rect {
5369 x: 0.0,
5370 y: 0.0,
5371 width: space.width,
5372 height: 0.0,
5373 },
5374 0.0,
5375 )),
5376 InlineContent::Marker { .. } => {
5377 Err(LayoutError::InvalidText(
5379 "Marker is text content, not a measurable object".into(),
5380 ))
5381 }
5382 _ => Err(LayoutError::InvalidText("Not a measurable object".into())),
5383 }
5384}
5385
5386fn apply_text_orientation(
5390 items: Arc<Vec<ShapedItem>>,
5391 constraints: &UnifiedConstraints,
5392) -> Result<Arc<Vec<ShapedItem>>, LayoutError> {
5393 if !constraints.is_vertical() {
5394 return Ok(items);
5395 }
5396
5397 let mut oriented_items = Vec::with_capacity(items.len());
5398 let writing_mode = constraints.writing_mode.unwrap_or_default();
5399
5400 for item in items.iter() {
5401 match item {
5402 ShapedItem::Cluster(cluster) => {
5403 let mut new_cluster = cluster.clone();
5404 let mut total_vertical_advance = 0.0;
5405
5406 for glyph in &mut new_cluster.glyphs {
5407 if glyph.vertical_advance > 0.0 {
5410 total_vertical_advance += glyph.vertical_advance;
5411 } else {
5412 let fallback_advance = cluster.style.line_height;
5414 glyph.vertical_advance = fallback_advance;
5415 glyph.vertical_offset = Point {
5417 x: -glyph.advance / 2.0,
5418 y: 0.0,
5419 };
5420 total_vertical_advance += fallback_advance;
5421 }
5422 }
5423 new_cluster.advance = total_vertical_advance;
5425 oriented_items.push(ShapedItem::Cluster(new_cluster));
5426 }
5427 ShapedItem::Object {
5429 source,
5430 bounds,
5431 baseline_offset,
5432 content,
5433 } => {
5434 let mut new_bounds = *bounds;
5435 std::mem::swap(&mut new_bounds.width, &mut new_bounds.height);
5436 oriented_items.push(ShapedItem::Object {
5437 source: *source,
5438 bounds: new_bounds,
5439 baseline_offset: *baseline_offset,
5440 content: content.clone(),
5441 });
5442 }
5443 _ => oriented_items.push(item.clone()),
5444 }
5445 }
5446
5447 Ok(Arc::new(oriented_items))
5448}
5449
5450pub fn get_item_vertical_metrics(item: &ShapedItem) -> (f32, f32) {
5456 match item {
5458 ShapedItem::Cluster(c) => {
5459 if c.glyphs.is_empty() {
5460 return (c.style.line_height, 0.0);
5462 }
5463 c.glyphs
5466 .iter()
5467 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
5468 let metrics = &glyph.font_metrics;
5469 if metrics.units_per_em == 0 {
5470 return (max_asc, max_desc);
5471 }
5472 let scale = glyph.style.font_size_px / metrics.units_per_em as f32;
5473 let item_asc = metrics.ascent * scale;
5474 let item_desc = (-metrics.descent * scale).max(0.0);
5477 (max_asc.max(item_asc), max_desc.max(item_desc))
5478 })
5479 }
5480 ShapedItem::Object {
5481 bounds,
5482 baseline_offset,
5483 ..
5484 } => {
5485 let ascent = bounds.height - *baseline_offset;
5487 let descent = *baseline_offset;
5488 (ascent.max(0.0), descent.max(0.0))
5489 }
5490 ShapedItem::CombinedBlock {
5491 bounds,
5492 baseline_offset,
5493 ..
5494 } => {
5495 let ascent = bounds.height - *baseline_offset;
5497 let descent = *baseline_offset;
5498 (ascent.max(0.0), descent.max(0.0))
5499 }
5500 _ => (0.0, 0.0), }
5502}
5503
5504fn calculate_line_metrics(items: &[ShapedItem]) -> (f32, f32) {
5507 items
5509 .iter()
5510 .fold((0.0f32, 0.0f32), |(max_asc, max_desc), item| {
5511 let (item_asc, item_desc) = get_item_vertical_metrics(item);
5512 (max_asc.max(item_asc), max_desc.max(item_desc))
5513 })
5514}
5515
5516pub fn perform_fragment_layout<T: ParsedFontTrait>(
5553 cursor: &mut BreakCursor,
5554 logical_items: &[LogicalItem],
5555 fragment_constraints: &UnifiedConstraints,
5556 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
5557 fonts: &LoadedFonts<T>,
5558) -> Result<UnifiedLayout, LayoutError> {
5559 if let Some(msgs) = debug_messages {
5560 msgs.push(LayoutDebugMessage::info(
5561 "\n--- Entering perform_fragment_layout ---".to_string(),
5562 ));
5563 msgs.push(LayoutDebugMessage::info(format!(
5564 "Constraints: available_width={:?}, available_height={:?}, columns={}, text_wrap={:?}",
5565 fragment_constraints.available_width,
5566 fragment_constraints.available_height,
5567 fragment_constraints.columns,
5568 fragment_constraints.text_wrap
5569 )));
5570 }
5571
5572 if fragment_constraints.text_wrap == TextWrap::Balance {
5575 if let Some(msgs) = debug_messages {
5576 msgs.push(LayoutDebugMessage::info(
5577 "Using Knuth-Plass algorithm for text-wrap: balance".to_string(),
5578 ));
5579 }
5580
5581 let shaped_items: Vec<ShapedItem> = cursor.drain_remaining();
5583
5584 let hyphenator = if fragment_constraints.hyphenation {
5585 fragment_constraints
5586 .hyphenation_language
5587 .and_then(|lang| get_hyphenator(lang).ok())
5588 } else {
5589 None
5590 };
5591
5592 return crate::text3::knuth_plass::kp_layout(
5594 &shaped_items,
5595 logical_items,
5596 fragment_constraints,
5597 hyphenator.as_ref(),
5598 fonts,
5599 );
5600 }
5601
5602 let hyphenator = if fragment_constraints.hyphenation {
5603 fragment_constraints
5604 .hyphenation_language
5605 .and_then(|lang| get_hyphenator(lang).ok())
5606 } else {
5607 None
5608 };
5609
5610 let mut positioned_items = Vec::new();
5611 let mut layout_bounds = Rect::default();
5612
5613 let num_columns = fragment_constraints.columns.max(1);
5614 let total_column_gap = fragment_constraints.column_gap * (num_columns - 1) as f32;
5615
5616 let column_width = match fragment_constraints.available_width {
5624 AvailableSpace::Definite(width) => (width - total_column_gap) / num_columns as f32,
5625 AvailableSpace::MinContent => {
5626 0.0
5628 }
5629 AvailableSpace::MaxContent => {
5630 f32::MAX / 2.0
5633 }
5634 };
5635 let mut current_column = 0;
5636 if let Some(msgs) = debug_messages {
5637 msgs.push(LayoutDebugMessage::info(format!(
5638 "Column width calculated: {}",
5639 column_width
5640 )));
5641 }
5642
5643 let base_direction = fragment_constraints.direction.unwrap_or(BidiDirection::Ltr);
5647
5648 if let Some(msgs) = debug_messages {
5649 msgs.push(LayoutDebugMessage::info(format!(
5650 "[PFLayout] Base direction: {:?} (from CSS), Text align: {:?}",
5651 base_direction, fragment_constraints.text_align
5652 )));
5653 }
5654
5655 'column_loop: while current_column < num_columns {
5656 if let Some(msgs) = debug_messages {
5657 msgs.push(LayoutDebugMessage::info(format!(
5658 "\n-- Starting Column {} --",
5659 current_column
5660 )));
5661 }
5662 let column_start_x =
5663 (column_width + fragment_constraints.column_gap) * current_column as f32;
5664 let mut line_top_y = 0.0;
5665 let mut line_index = 0;
5666 let mut empty_segment_count = 0; const MAX_EMPTY_SEGMENTS: usize = 1000; while !cursor.is_done() {
5670 if let Some(max_height) = fragment_constraints.available_height {
5671 if line_top_y >= max_height {
5672 if let Some(msgs) = debug_messages {
5673 msgs.push(LayoutDebugMessage::info(format!(
5674 " Column full (pen {} >= height {}), breaking to next column.",
5675 line_top_y, max_height
5676 )));
5677 }
5678 break;
5679 }
5680 }
5681
5682 if let Some(clamp) = fragment_constraints.line_clamp {
5683 if line_index >= clamp.get() {
5684 break;
5685 }
5686 }
5687
5688 let mut column_constraints = fragment_constraints.clone();
5690 column_constraints.available_width = AvailableSpace::Definite(column_width);
5691 let line_constraints = get_line_constraints(
5692 line_top_y,
5693 fragment_constraints.line_height,
5694 &column_constraints,
5695 debug_messages,
5696 );
5697
5698 if line_constraints.segments.is_empty() {
5699 empty_segment_count += 1;
5700 if let Some(msgs) = debug_messages {
5701 msgs.push(LayoutDebugMessage::info(format!(
5702 " No available segments at y={}, skipping to next line. (empty count: \
5703 {}/{})",
5704 line_top_y, empty_segment_count, MAX_EMPTY_SEGMENTS
5705 )));
5706 }
5707
5708 if empty_segment_count >= MAX_EMPTY_SEGMENTS {
5710 if let Some(msgs) = debug_messages {
5711 msgs.push(LayoutDebugMessage::warning(format!(
5712 " [WARN] Reached maximum empty segment count ({}). Breaking to \
5713 prevent infinite loop.",
5714 MAX_EMPTY_SEGMENTS
5715 )));
5716 msgs.push(LayoutDebugMessage::warning(
5717 " This likely means the shape constraints are too restrictive or \
5718 positioned incorrectly."
5719 .to_string(),
5720 ));
5721 msgs.push(LayoutDebugMessage::warning(format!(
5722 " Current y={}, shape boundaries might be outside this range.",
5723 line_top_y
5724 )));
5725 }
5726 break;
5727 }
5728
5729 if !fragment_constraints.shape_boundaries.is_empty() && empty_segment_count > 50 {
5732 let max_shape_y: f32 = fragment_constraints
5734 .shape_boundaries
5735 .iter()
5736 .map(|shape| {
5737 match shape {
5738 ShapeBoundary::Circle { center, radius } => center.y + radius,
5739 ShapeBoundary::Ellipse { center, radii } => center.y + radii.height,
5740 ShapeBoundary::Polygon { points } => {
5741 points.iter().map(|p| p.y).fold(0.0, f32::max)
5742 }
5743 ShapeBoundary::Rectangle(rect) => rect.y + rect.height,
5744 ShapeBoundary::Path { .. } => f32::MAX, }
5746 })
5747 .fold(0.0, f32::max);
5748
5749 if line_top_y > max_shape_y + 100.0 {
5750 if let Some(msgs) = debug_messages {
5751 msgs.push(LayoutDebugMessage::info(format!(
5752 " [INFO] Current y={} is far beyond maximum shape extent y={}. \
5753 Breaking layout.",
5754 line_top_y, max_shape_y
5755 )));
5756 msgs.push(LayoutDebugMessage::info(
5757 " Shape boundaries exist but no segments available - text cannot \
5758 fit in shape."
5759 .to_string(),
5760 ));
5761 }
5762 break;
5763 }
5764 }
5765
5766 line_top_y += fragment_constraints.line_height;
5767 continue;
5768 }
5769
5770 empty_segment_count = 0;
5772
5773 let (mut line_items, was_hyphenated) =
5778 break_one_line(cursor, &line_constraints, false, hyphenator.as_ref(), fonts);
5779 if line_items.is_empty() {
5780 if let Some(msgs) = debug_messages {
5781 msgs.push(LayoutDebugMessage::info(
5782 " Break returned no items. Ending column.".to_string(),
5783 ));
5784 }
5785 break;
5786 }
5787
5788 let line_text_before_rev: String = line_items
5789 .iter()
5790 .filter_map(|i| i.as_cluster())
5791 .map(|c| c.text.as_str())
5792 .collect();
5793 if let Some(msgs) = debug_messages {
5794 msgs.push(LayoutDebugMessage::info(format!(
5795 "[PFLayout] Line items from breaker (visual order): [{}]",
5797 line_text_before_rev
5798 )));
5799 }
5800
5801 let (mut line_pos_items, line_height) = position_one_line(
5802 line_items,
5803 &line_constraints,
5804 line_top_y,
5805 line_index,
5806 fragment_constraints.text_align,
5807 base_direction,
5808 cursor.is_done() && !was_hyphenated,
5809 fragment_constraints,
5810 debug_messages,
5811 fonts,
5812 );
5813
5814 for item in &mut line_pos_items {
5815 item.position.x += column_start_x;
5816 }
5817
5818 line_top_y += line_height.max(fragment_constraints.line_height);
5819 line_index += 1;
5820 positioned_items.extend(line_pos_items);
5821 }
5822 current_column += 1;
5823 }
5824
5825 if let Some(msgs) = debug_messages {
5826 msgs.push(LayoutDebugMessage::info(format!(
5827 "--- Exiting perform_fragment_layout, positioned {} items ---",
5828 positioned_items.len()
5829 )));
5830 }
5831
5832 let layout = UnifiedLayout {
5833 items: positioned_items,
5834 overflow: OverflowInfo::default(),
5835 };
5836
5837 let calculated_bounds = layout.bounds();
5839
5840 if let Some(msgs) = debug_messages {
5841 msgs.push(LayoutDebugMessage::info(format!(
5842 "--- Calculated bounds: width={}, height={} ---",
5843 calculated_bounds.width, calculated_bounds.height
5844 )));
5845 }
5846
5847 Ok(layout)
5848}
5849
5850pub fn break_one_line<T: ParsedFontTrait>(
5901 cursor: &mut BreakCursor,
5902 line_constraints: &LineConstraints,
5903 is_vertical: bool,
5904 hyphenator: Option<&Standard>,
5905 fonts: &LoadedFonts<T>,
5906) -> (Vec<ShapedItem>, bool) {
5907 let mut line_items = Vec::new();
5908 let mut current_width = 0.0;
5909
5910 if cursor.is_done() {
5911 return (Vec::new(), false);
5912 }
5913
5914 while !cursor.is_done() {
5918 let next_unit = cursor.peek_next_unit();
5919 if next_unit.is_empty() {
5920 break;
5921 }
5922 if next_unit.len() == 1 && is_word_separator(&next_unit[0]) {
5924 cursor.consume(1);
5926 } else {
5927 break;
5928 }
5929 }
5930
5931 loop {
5932 let next_unit = cursor.peek_next_unit();
5934 if next_unit.is_empty() {
5935 break; }
5937
5938 if let Some(ShapedItem::Break { .. }) = next_unit.first() {
5940 line_items.push(next_unit[0].clone());
5941 cursor.consume(1);
5942 return (line_items, false);
5943 }
5944
5945 let unit_width: f32 = next_unit
5946 .iter()
5947 .map(|item| get_item_measure(item, is_vertical))
5948 .sum();
5949 let available_width = line_constraints.total_available - current_width;
5950
5951 if unit_width <= available_width {
5953 line_items.extend_from_slice(&next_unit);
5954 current_width += unit_width;
5955 cursor.consume(next_unit.len());
5956 } else {
5957 if let Some(hyphenator) = hyphenator {
5959 if !is_break_opportunity(next_unit.last().unwrap()) {
5961 if let Some(hyphenation_result) = try_hyphenate_word_cluster(
5962 &next_unit,
5963 available_width,
5964 is_vertical,
5965 hyphenator,
5966 fonts,
5967 ) {
5968 line_items.extend(hyphenation_result.line_part);
5969 cursor.consume(next_unit.len());
5971 cursor.partial_remainder = hyphenation_result.remainder_part;
5973 return (line_items, true);
5974 }
5975 }
5976 }
5977
5978 if line_items.is_empty() {
5981 line_items.push(next_unit[0].clone());
5982 cursor.consume(1);
5983 }
5984 break;
5985 }
5986 }
5987
5988 (line_items, false)
5989}
5990
5991#[derive(Clone)]
5993pub struct HyphenationBreak {
5994 pub char_len_on_line: usize,
5996 pub width_on_line: f32,
5998 pub line_part: Vec<ShapedItem>,
6000 pub hyphen_item: ShapedItem,
6002 pub remainder_part: Vec<ShapedItem>,
6005}
6006
6007pub fn find_all_hyphenation_breaks<T: ParsedFontTrait>(
6009 word_clusters: &[ShapedCluster],
6010 hyphenator: &Standard,
6011 is_vertical: bool, fonts: &LoadedFonts<T>,
6013) -> Option<Vec<HyphenationBreak>> {
6014 if word_clusters.is_empty() {
6015 return None;
6016 }
6017
6018 let mut word_string = String::new();
6020 let mut char_map = Vec::new();
6021 let mut current_width = 0.0;
6022
6023 for (cluster_idx, cluster) in word_clusters.iter().enumerate() {
6024 for (char_byte_offset, _ch) in cluster.text.char_indices() {
6025 let glyph_idx = cluster
6026 .glyphs
6027 .iter()
6028 .rposition(|g| g.cluster_offset as usize <= char_byte_offset)
6029 .unwrap_or(0);
6030 let glyph = &cluster.glyphs[glyph_idx];
6031
6032 let num_chars_in_glyph = cluster.text[glyph.cluster_offset as usize..]
6033 .chars()
6034 .count();
6035 let advance_per_char = if is_vertical {
6036 glyph.vertical_advance
6037 } else {
6038 glyph.advance
6039 } / (num_chars_in_glyph as f32).max(1.0);
6040
6041 current_width += advance_per_char;
6042 char_map.push((cluster_idx, glyph_idx, current_width));
6043 }
6044 word_string.push_str(&cluster.text);
6045 }
6046
6047 let opportunities = hyphenator.hyphenate(&word_string);
6049 if opportunities.breaks.is_empty() {
6050 return None;
6051 }
6052
6053 let last_cluster = word_clusters.last().unwrap();
6054 let last_glyph = last_cluster.glyphs.last().unwrap();
6055 let style = last_cluster.style.clone();
6056
6057 let font = fonts.get_by_hash(last_glyph.font_hash)?;
6059 let (hyphen_glyph_id, hyphen_advance) =
6060 font.get_hyphen_glyph_and_advance(style.font_size_px)?;
6061
6062 let mut possible_breaks = Vec::new();
6063
6064 for &break_char_idx in &opportunities.breaks {
6066 if break_char_idx == 0 || break_char_idx > char_map.len() {
6069 continue;
6070 }
6071
6072 let (_, _, width_at_break) = char_map[break_char_idx - 1];
6073
6074 let line_part: Vec<ShapedItem> = word_clusters[..break_char_idx]
6076 .iter()
6077 .map(|c| ShapedItem::Cluster(c.clone()))
6078 .collect();
6079
6080 let remainder_part: Vec<ShapedItem> = word_clusters[break_char_idx..]
6082 .iter()
6083 .map(|c| ShapedItem::Cluster(c.clone()))
6084 .collect();
6085
6086 let hyphen_item = ShapedItem::Cluster(ShapedCluster {
6087 text: "-".to_string(),
6088 source_cluster_id: GraphemeClusterId {
6089 source_run: u32::MAX,
6090 start_byte_in_run: u32::MAX,
6091 },
6092 source_content_index: ContentIndex {
6093 run_index: u32::MAX,
6094 item_index: u32::MAX,
6095 },
6096 source_node_id: None, glyphs: vec![ShapedGlyph {
6098 kind: GlyphKind::Hyphen,
6099 glyph_id: hyphen_glyph_id,
6100 font_hash: last_glyph.font_hash,
6101 font_metrics: last_glyph.font_metrics.clone(),
6102 cluster_offset: 0,
6103 script: Script::Latin,
6104 advance: hyphen_advance,
6105 kerning: 0.0,
6106 offset: Point::default(),
6107 style: style.clone(),
6108 vertical_advance: hyphen_advance,
6109 vertical_offset: Point::default(),
6110 }],
6111 advance: hyphen_advance,
6112 direction: BidiDirection::Ltr,
6113 style: style.clone(),
6114 marker_position_outside: None,
6115 });
6116
6117 possible_breaks.push(HyphenationBreak {
6118 char_len_on_line: break_char_idx,
6119 width_on_line: width_at_break + hyphen_advance,
6120 line_part,
6121 hyphen_item,
6122 remainder_part,
6123 });
6124 }
6125
6126 Some(possible_breaks)
6127}
6128
6129fn try_hyphenate_word_cluster<T: ParsedFontTrait>(
6131 word_items: &[ShapedItem],
6132 remaining_width: f32,
6133 is_vertical: bool,
6134 hyphenator: &Standard,
6135 fonts: &LoadedFonts<T>,
6136) -> Option<HyphenationResult> {
6137 let word_clusters: Vec<ShapedCluster> = word_items
6138 .iter()
6139 .filter_map(|item| item.as_cluster().cloned())
6140 .collect();
6141
6142 if word_clusters.is_empty() {
6143 return None;
6144 }
6145
6146 let all_breaks = find_all_hyphenation_breaks(&word_clusters, hyphenator, is_vertical, fonts)?;
6147
6148 if let Some(best_break) = all_breaks
6149 .into_iter()
6150 .rfind(|b| b.width_on_line <= remaining_width)
6151 {
6152 let mut line_part = best_break.line_part;
6153 line_part.push(best_break.hyphen_item);
6154
6155 return Some(HyphenationResult {
6156 line_part,
6157 remainder_part: best_break.remainder_part,
6158 });
6159 }
6160
6161 None
6162}
6163
6164pub fn position_one_line<T: ParsedFontTrait>(
6235 line_items: Vec<ShapedItem>,
6236 line_constraints: &LineConstraints,
6237 line_top_y: f32,
6238 line_index: usize,
6239 text_align: TextAlign,
6240 base_direction: BidiDirection,
6241 is_last_line: bool,
6242 constraints: &UnifiedConstraints,
6243 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6244 fonts: &LoadedFonts<T>,
6245) -> (Vec<PositionedItem>, f32) {
6246 let line_text: String = line_items
6247 .iter()
6248 .filter_map(|i| i.as_cluster())
6249 .map(|c| c.text.as_str())
6250 .collect();
6251 if let Some(msgs) = debug_messages {
6252 msgs.push(LayoutDebugMessage::info(format!(
6253 "\n--- Entering position_one_line for line: [{}] ---",
6254 line_text
6255 )));
6256 }
6257 let physical_align = match (text_align, base_direction) {
6259 (TextAlign::Start, BidiDirection::Ltr) => TextAlign::Left,
6260 (TextAlign::Start, BidiDirection::Rtl) => TextAlign::Right,
6261 (TextAlign::End, BidiDirection::Ltr) => TextAlign::Right,
6262 (TextAlign::End, BidiDirection::Rtl) => TextAlign::Left,
6263 (other, _) => other,
6265 };
6266 if let Some(msgs) = debug_messages {
6267 msgs.push(LayoutDebugMessage::info(format!(
6268 "[Pos1Line] Physical align: {:?}",
6269 physical_align
6270 )));
6271 }
6272
6273 if line_items.is_empty() {
6274 return (Vec::new(), 0.0);
6275 }
6276 let mut positioned = Vec::new();
6277 let is_vertical = constraints.is_vertical();
6278
6279 let (line_ascent, line_descent) = calculate_line_metrics(&line_items);
6281 let line_box_height = line_ascent + line_descent;
6282
6283 let line_baseline_y = line_top_y + line_ascent;
6285
6286 let mut item_cursor = 0;
6288 let is_first_line_of_para = line_index == 0; for (segment_idx, segment) in line_constraints.segments.iter().enumerate() {
6291 if item_cursor >= line_items.len() {
6292 break;
6293 }
6294
6295 let mut segment_items = Vec::new();
6297 let mut current_segment_width = 0.0;
6298 while item_cursor < line_items.len() {
6299 let item = &line_items[item_cursor];
6300 let item_measure = get_item_measure(item, is_vertical);
6301 if current_segment_width + item_measure > segment.width && !segment_items.is_empty() {
6303 break;
6304 }
6305 segment_items.push(item.clone());
6306 current_segment_width += item_measure;
6307 item_cursor += 1;
6308 }
6309
6310 if segment_items.is_empty() {
6311 continue;
6312 }
6313
6314 let (extra_word_spacing, extra_char_spacing) = if constraints.text_justify
6316 != JustifyContent::None
6317 && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
6318 && constraints.text_justify != JustifyContent::Kashida
6319 {
6320 let segment_line_constraints = LineConstraints {
6321 segments: vec![segment.clone()],
6322 total_available: segment.width,
6323 };
6324 calculate_justification_spacing(
6325 &segment_items,
6326 &segment_line_constraints,
6327 constraints.text_justify,
6328 is_vertical,
6329 )
6330 } else {
6331 (0.0, 0.0)
6332 };
6333
6334 let justified_segment_items = if constraints.text_justify == JustifyContent::Kashida
6336 && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
6337 {
6338 let segment_line_constraints = LineConstraints {
6339 segments: vec![segment.clone()],
6340 total_available: segment.width,
6341 };
6342 justify_kashida_and_rebuild(
6343 segment_items,
6344 &segment_line_constraints,
6345 is_vertical,
6346 debug_messages,
6347 fonts,
6348 )
6349 } else {
6350 segment_items
6351 };
6352
6353 let final_segment_width: f32 = justified_segment_items
6355 .iter()
6356 .map(|item| get_item_measure(item, is_vertical))
6357 .sum();
6358
6359 let remaining_space = segment.width - final_segment_width;
6361
6362 let is_indefinite_width = segment.width.is_infinite() || segment.width > 1e30;
6368 let alignment_offset = if is_indefinite_width {
6369 0.0 } else {
6371 match physical_align {
6372 TextAlign::Center => remaining_space / 2.0,
6373 TextAlign::Right => remaining_space,
6374 _ => 0.0, }
6376 };
6377
6378 let mut main_axis_pen = segment.start_x + alignment_offset;
6379 if let Some(msgs) = debug_messages {
6380 msgs.push(LayoutDebugMessage::info(format!(
6381 "[Pos1Line] Segment width: {}, Item width: {}, Remaining space: {}, Initial pen: \
6382 {}",
6383 segment.width, final_segment_width, remaining_space, main_axis_pen
6384 )));
6385 }
6386
6387 if is_first_line_of_para && segment_idx == 0 {
6389 main_axis_pen += constraints.text_indent;
6390 }
6391
6392 let total_marker_width: f32 = justified_segment_items
6395 .iter()
6396 .filter_map(|item| {
6397 if let ShapedItem::Cluster(c) = item {
6398 if c.marker_position_outside == Some(true) {
6399 return Some(get_item_measure(item, is_vertical));
6400 }
6401 }
6402 None
6403 })
6404 .sum();
6405
6406 let marker_spacing = 4.0; let mut marker_pen = if total_marker_width > 0.0 {
6409 -(total_marker_width + marker_spacing)
6410 } else {
6411 0.0
6412 };
6413
6414 for item in justified_segment_items {
6440 let (item_ascent, item_descent) = get_item_vertical_metrics(&item);
6441 let item_baseline_pos = match constraints.vertical_align {
6442 VerticalAlign::Top => line_top_y + item_ascent,
6443 VerticalAlign::Middle => {
6444 line_top_y + (line_box_height / 2.0) - ((item_ascent + item_descent) / 2.0)
6445 + item_ascent
6446 }
6447 VerticalAlign::Bottom => line_top_y + line_box_height - item_descent,
6448 _ => line_baseline_y, };
6450
6451 let item_measure = get_item_measure(&item, is_vertical);
6453
6454 let position = if is_vertical {
6455 Point {
6456 x: item_baseline_pos - item_ascent,
6457 y: main_axis_pen,
6458 }
6459 } else {
6460 if let Some(msgs) = debug_messages {
6461 msgs.push(LayoutDebugMessage::info(format!(
6462 "[Pos1Line] is_vertical=false, main_axis_pen={}, item_baseline_pos={}, \
6463 item_ascent={}",
6464 main_axis_pen, item_baseline_pos, item_ascent
6465 )));
6466 }
6467
6468 let x_position = if let ShapedItem::Cluster(cluster) = &item {
6470 if cluster.marker_position_outside == Some(true) {
6471 let marker_width = item_measure;
6473 if let Some(msgs) = debug_messages {
6474 msgs.push(LayoutDebugMessage::info(format!(
6475 "[Pos1Line] Outside marker detected! width={}, positioning at \
6476 marker_pen={}",
6477 marker_width, marker_pen
6478 )));
6479 }
6480 let pos = marker_pen;
6481 marker_pen += marker_width; pos
6483 } else {
6484 main_axis_pen
6485 }
6486 } else {
6487 main_axis_pen
6488 };
6489
6490 Point {
6491 y: item_baseline_pos - item_ascent,
6492 x: x_position,
6493 }
6494 };
6495
6496 let item_text = item
6498 .as_cluster()
6499 .map(|c| c.text.as_str())
6500 .unwrap_or("[OBJ]");
6501 if let Some(msgs) = debug_messages {
6502 msgs.push(LayoutDebugMessage::info(format!(
6503 "[Pos1Line] Positioning item '{}' at pen_x={}",
6504 item_text, main_axis_pen
6505 )));
6506 }
6507 positioned.push(PositionedItem {
6508 item: item.clone(),
6509 position,
6510 line_index,
6511 });
6512
6513 let is_outside_marker = if let ShapedItem::Cluster(c) = &item {
6515 c.marker_position_outside == Some(true)
6516 } else {
6517 false
6518 };
6519
6520 if !is_outside_marker {
6521 main_axis_pen += item_measure;
6522 }
6523
6524 if !is_outside_marker && extra_char_spacing > 0.0 && can_justify_after(&item) {
6526 main_axis_pen += extra_char_spacing;
6527 }
6528 if let ShapedItem::Cluster(c) = &item {
6529 if !is_outside_marker {
6530 let letter_spacing_px = match c.style.letter_spacing {
6531 Spacing::Px(px) => px as f32,
6532 Spacing::Em(em) => em * c.style.font_size_px,
6533 };
6534 main_axis_pen += letter_spacing_px;
6535 if is_word_separator(&item) {
6536 let word_spacing_px = match c.style.word_spacing {
6537 Spacing::Px(px) => px as f32,
6538 Spacing::Em(em) => em * c.style.font_size_px,
6539 };
6540 main_axis_pen += word_spacing_px;
6541 main_axis_pen += extra_word_spacing;
6542 }
6543 }
6544 }
6545 }
6546 }
6547
6548 (positioned, line_box_height)
6549}
6550
6551fn calculate_alignment_offset(
6553 items: &[ShapedItem],
6554 line_constraints: &LineConstraints,
6555 align: TextAlign,
6556 is_vertical: bool,
6557 constraints: &UnifiedConstraints,
6558) -> f32 {
6559 if let Some(segment) = line_constraints.segments.first() {
6561 let total_width: f32 = items
6562 .iter()
6563 .map(|item| get_item_measure(item, is_vertical))
6564 .sum();
6565
6566 let available_width = if constraints.segment_alignment == SegmentAlignment::Total {
6567 line_constraints.total_available
6568 } else {
6569 segment.width
6570 };
6571
6572 if total_width >= available_width {
6573 return 0.0; }
6575
6576 let remaining_space = available_width - total_width;
6577
6578 match align {
6579 TextAlign::Center => remaining_space / 2.0,
6580 TextAlign::Right => remaining_space,
6581 _ => 0.0, }
6583 } else {
6584 0.0
6585 }
6586}
6587
6588fn calculate_justification_spacing(
6603 items: &[ShapedItem],
6604 line_constraints: &LineConstraints,
6605 text_justify: JustifyContent,
6606 is_vertical: bool,
6607) -> (f32, f32) {
6608 let total_width: f32 = items
6610 .iter()
6611 .map(|item| get_item_measure(item, is_vertical))
6612 .sum();
6613 let available_width = line_constraints.total_available;
6614
6615 if total_width >= available_width || available_width <= 0.0 {
6616 return (0.0, 0.0);
6617 }
6618
6619 let extra_space = available_width - total_width;
6620
6621 match text_justify {
6622 JustifyContent::InterWord => {
6623 let space_count = items.iter().filter(|item| is_word_separator(item)).count();
6625 if space_count > 0 {
6626 (extra_space / space_count as f32, 0.0)
6627 } else {
6628 (0.0, 0.0) }
6630 }
6631 JustifyContent::InterCharacter | JustifyContent::Distribute => {
6632 let gap_count = items
6634 .iter()
6635 .enumerate()
6636 .filter(|(i, item)| *i < items.len() - 1 && can_justify_after(item))
6637 .count();
6638 if gap_count > 0 {
6639 (0.0, extra_space / gap_count as f32)
6640 } else {
6641 (0.0, 0.0) }
6643 }
6644 _ => (0.0, 0.0),
6646 }
6647}
6648
6649pub fn justify_kashida_and_rebuild<T: ParsedFontTrait>(
6655 items: Vec<ShapedItem>,
6656 line_constraints: &LineConstraints,
6657 is_vertical: bool,
6658 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6659 fonts: &LoadedFonts<T>,
6660) -> Vec<ShapedItem> {
6661 if let Some(msgs) = debug_messages {
6662 msgs.push(LayoutDebugMessage::info(
6663 "\n--- Entering justify_kashida_and_rebuild ---".to_string(),
6664 ));
6665 }
6666 let total_width: f32 = items
6667 .iter()
6668 .map(|item| get_item_measure(item, is_vertical))
6669 .sum();
6670 let available_width = line_constraints.total_available;
6671 if let Some(msgs) = debug_messages {
6672 msgs.push(LayoutDebugMessage::info(format!(
6673 "Total item width: {}, Available width: {}",
6674 total_width, available_width
6675 )));
6676 }
6677
6678 if total_width >= available_width || available_width <= 0.0 {
6679 if let Some(msgs) = debug_messages {
6680 msgs.push(LayoutDebugMessage::info(
6681 "No justification needed (line is full or invalid).".to_string(),
6682 ));
6683 }
6684 return items;
6685 }
6686
6687 let extra_space = available_width - total_width;
6688 if let Some(msgs) = debug_messages {
6689 msgs.push(LayoutDebugMessage::info(format!(
6690 "Extra space to fill: {}",
6691 extra_space
6692 )));
6693 }
6694
6695 let font_info = items.iter().find_map(|item| {
6696 if let ShapedItem::Cluster(c) = item {
6697 if let Some(glyph) = c.glyphs.first() {
6698 if glyph.script == Script::Arabic {
6699 if let Some(font) = fonts.get_by_hash(glyph.font_hash) {
6701 return Some((
6702 font.clone(),
6703 glyph.font_hash,
6704 glyph.font_metrics.clone(),
6705 glyph.style.clone(),
6706 ));
6707 }
6708 }
6709 }
6710 }
6711 None
6712 });
6713
6714 let (font, font_hash, font_metrics, style) = match font_info {
6715 Some(info) => {
6716 if let Some(msgs) = debug_messages {
6717 msgs.push(LayoutDebugMessage::info(
6718 "Found Arabic font for kashida.".to_string(),
6719 ));
6720 }
6721 info
6722 }
6723 None => {
6724 if let Some(msgs) = debug_messages {
6725 msgs.push(LayoutDebugMessage::info(
6726 "No Arabic font found on line. Cannot insert kashidas.".to_string(),
6727 ));
6728 }
6729 return items;
6730 }
6731 };
6732
6733 let (kashida_glyph_id, kashida_advance) =
6734 match font.get_kashida_glyph_and_advance(style.font_size_px) {
6735 Some((id, adv)) if adv > 0.0 => {
6736 if let Some(msgs) = debug_messages {
6737 msgs.push(LayoutDebugMessage::info(format!(
6738 "Font provides kashida glyph with advance {}",
6739 adv
6740 )));
6741 }
6742 (id, adv)
6743 }
6744 _ => {
6745 if let Some(msgs) = debug_messages {
6746 msgs.push(LayoutDebugMessage::info(
6747 "Font does not support kashida justification.".to_string(),
6748 ));
6749 }
6750 return items;
6751 }
6752 };
6753
6754 let opportunity_indices: Vec<usize> = items
6755 .windows(2)
6756 .enumerate()
6757 .filter_map(|(i, window)| {
6758 if let (ShapedItem::Cluster(cur), ShapedItem::Cluster(next)) = (&window[0], &window[1])
6759 {
6760 if is_arabic_cluster(cur)
6761 && is_arabic_cluster(next)
6762 && !is_word_separator(&window[1])
6763 {
6764 return Some(i + 1);
6765 }
6766 }
6767 None
6768 })
6769 .collect();
6770
6771 if let Some(msgs) = debug_messages {
6772 msgs.push(LayoutDebugMessage::info(format!(
6773 "Found {} kashida insertion opportunities at indices: {:?}",
6774 opportunity_indices.len(),
6775 opportunity_indices
6776 )));
6777 }
6778
6779 if opportunity_indices.is_empty() {
6780 if let Some(msgs) = debug_messages {
6781 msgs.push(LayoutDebugMessage::info(
6782 "No opportunities found. Exiting.".to_string(),
6783 ));
6784 }
6785 return items;
6786 }
6787
6788 let num_kashidas_to_insert = (extra_space / kashida_advance).floor() as usize;
6789 if let Some(msgs) = debug_messages {
6790 msgs.push(LayoutDebugMessage::info(format!(
6791 "Calculated number of kashidas to insert: {}",
6792 num_kashidas_to_insert
6793 )));
6794 }
6795
6796 if num_kashidas_to_insert == 0 {
6797 return items;
6798 }
6799
6800 let kashidas_per_point = num_kashidas_to_insert / opportunity_indices.len();
6801 let mut remainder = num_kashidas_to_insert % opportunity_indices.len();
6802 if let Some(msgs) = debug_messages {
6803 msgs.push(LayoutDebugMessage::info(format!(
6804 "Distributing kashidas: {} per point, with {} remainder.",
6805 kashidas_per_point, remainder
6806 )));
6807 }
6808
6809 let kashida_item = {
6810 let kashida_glyph = ShapedGlyph {
6812 kind: GlyphKind::Kashida {
6813 width: kashida_advance,
6814 },
6815 glyph_id: kashida_glyph_id,
6816 font_hash,
6817 font_metrics: font_metrics.clone(),
6818 style: style.clone(),
6819 script: Script::Arabic,
6820 advance: kashida_advance,
6821 kerning: 0.0,
6822 cluster_offset: 0,
6823 offset: Point::default(),
6824 vertical_advance: 0.0,
6825 vertical_offset: Point::default(),
6826 };
6827 ShapedItem::Cluster(ShapedCluster {
6828 text: "\u{0640}".to_string(),
6829 source_cluster_id: GraphemeClusterId {
6830 source_run: u32::MAX,
6831 start_byte_in_run: u32::MAX,
6832 },
6833 source_content_index: ContentIndex {
6834 run_index: u32::MAX,
6835 item_index: u32::MAX,
6836 },
6837 source_node_id: None, glyphs: vec![kashida_glyph],
6839 advance: kashida_advance,
6840 direction: BidiDirection::Ltr,
6841 style,
6842 marker_position_outside: None,
6843 })
6844 };
6845
6846 let mut new_items = Vec::with_capacity(items.len() + num_kashidas_to_insert);
6847 let mut last_copy_idx = 0;
6848 for &point in &opportunity_indices {
6849 new_items.extend_from_slice(&items[last_copy_idx..point]);
6850 let mut num_to_insert = kashidas_per_point;
6851 if remainder > 0 {
6852 num_to_insert += 1;
6853 remainder -= 1;
6854 }
6855 for _ in 0..num_to_insert {
6856 new_items.push(kashida_item.clone());
6857 }
6858 last_copy_idx = point;
6859 }
6860 new_items.extend_from_slice(&items[last_copy_idx..]);
6861
6862 if let Some(msgs) = debug_messages {
6863 msgs.push(LayoutDebugMessage::info(format!(
6864 "--- Exiting justify_kashida_and_rebuild, new item count: {} ---",
6865 new_items.len()
6866 )));
6867 }
6868 new_items
6869}
6870
6871fn is_arabic_cluster(cluster: &ShapedCluster) -> bool {
6873 cluster.glyphs.iter().any(|g| g.script == Script::Arabic)
6876}
6877
6878pub fn is_word_separator(item: &ShapedItem) -> bool {
6880 if let ShapedItem::Cluster(c) = item {
6881 c.text.chars().any(|g| g.is_whitespace())
6884 } else {
6885 false
6886 }
6887}
6888
6889fn can_justify_after(item: &ShapedItem) -> bool {
6891 if let ShapedItem::Cluster(c) = item {
6892 c.text.chars().last().map_or(false, |g| {
6893 !g.is_whitespace() && classify_character(g as u32) != CharacterClass::Combining
6894 })
6895 } else {
6896 !matches!(item, ShapedItem::Break { .. })
6898 }
6899}
6900
6901fn classify_character(codepoint: u32) -> CharacterClass {
6904 match codepoint {
6905 0x0020 | 0x00A0 | 0x3000 => CharacterClass::Space,
6906 0x0021..=0x002F | 0x003A..=0x0040 | 0x005B..=0x0060 | 0x007B..=0x007E => {
6907 CharacterClass::Punctuation
6908 }
6909 0x4E00..=0x9FFF | 0x3400..=0x4DBF => CharacterClass::Ideograph,
6910 0x0300..=0x036F | 0x1AB0..=0x1AFF => CharacterClass::Combining,
6911 0x1800..=0x18AF => CharacterClass::Letter,
6913 _ => CharacterClass::Letter,
6914 }
6915}
6916
6917pub fn get_item_measure(item: &ShapedItem, is_vertical: bool) -> f32 {
6919 match item {
6920 ShapedItem::Cluster(c) => {
6921 let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
6925 c.advance + total_kerning
6926 }
6927 ShapedItem::Object { bounds, .. }
6928 | ShapedItem::CombinedBlock { bounds, .. }
6929 | ShapedItem::Tab { bounds, .. } => {
6930 if is_vertical {
6931 bounds.height
6932 } else {
6933 bounds.width
6934 }
6935 }
6936 ShapedItem::Break { .. } => 0.0,
6937 }
6938}
6939
6940fn get_item_bounds(item: &PositionedItem) -> Rect {
6942 let measure = get_item_measure(&item.item, false); let cross_measure = match &item.item {
6944 ShapedItem::Object { bounds, .. } => bounds.height,
6945 _ => 20.0, };
6947 Rect {
6948 x: item.position.x,
6949 y: item.position.y,
6950 width: measure,
6951 height: cross_measure,
6952 }
6953}
6954
6955fn get_line_constraints(
6958 line_y: f32,
6959 line_height: f32,
6960 constraints: &UnifiedConstraints,
6961 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6962) -> LineConstraints {
6963 if let Some(msgs) = debug_messages {
6964 msgs.push(LayoutDebugMessage::info(format!(
6965 "\n--- Entering get_line_constraints for y={} ---",
6966 line_y
6967 )));
6968 }
6969
6970 let mut available_segments = Vec::new();
6971 if constraints.shape_boundaries.is_empty() {
6972 let segment_width = match constraints.available_width {
6978 AvailableSpace::Definite(w) => w, AvailableSpace::MaxContent => f32::MAX / 2.0, AvailableSpace::MinContent => 0.0, };
6982 available_segments.push(LineSegment {
6985 start_x: 0.0,
6986 width: segment_width,
6987 priority: 0,
6988 });
6989 } else {
6990 }
6992
6993 if let Some(msgs) = debug_messages {
6994 msgs.push(LayoutDebugMessage::info(format!(
6995 "Initial available segments: {:?}",
6996 available_segments
6997 )));
6998 }
6999
7000 for (idx, exclusion) in constraints.shape_exclusions.iter().enumerate() {
7001 if let Some(msgs) = debug_messages {
7002 msgs.push(LayoutDebugMessage::info(format!(
7003 "Applying exclusion #{}: {:?}",
7004 idx, exclusion
7005 )));
7006 }
7007 let exclusion_spans =
7008 get_shape_horizontal_spans(exclusion, line_y, line_height).unwrap_or_default();
7009 if let Some(msgs) = debug_messages {
7010 msgs.push(LayoutDebugMessage::info(format!(
7011 " Exclusion spans at y={}: {:?}",
7012 line_y, exclusion_spans
7013 )));
7014 }
7015
7016 if exclusion_spans.is_empty() {
7017 continue;
7018 }
7019
7020 let mut next_segments = Vec::new();
7021 for (excl_start, excl_end) in exclusion_spans {
7022 for segment in &available_segments {
7023 let seg_start = segment.start_x;
7024 let seg_end = segment.start_x + segment.width;
7025
7026 if seg_end > excl_start && seg_start < excl_end {
7028 if seg_start < excl_start {
7029 next_segments.push(LineSegment {
7031 start_x: seg_start,
7032 width: excl_start - seg_start,
7033 priority: segment.priority,
7034 });
7035 }
7036 if seg_end > excl_end {
7037 next_segments.push(LineSegment {
7039 start_x: excl_end,
7040 width: seg_end - excl_end,
7041 priority: segment.priority,
7042 });
7043 }
7044 } else {
7045 next_segments.push(segment.clone()); }
7047 }
7048 available_segments = merge_segments(next_segments);
7049 next_segments = Vec::new();
7050 }
7051 if let Some(msgs) = debug_messages {
7052 msgs.push(LayoutDebugMessage::info(format!(
7053 " Segments after exclusion #{}: {:?}",
7054 idx, available_segments
7055 )));
7056 }
7057 }
7058
7059 let total_width = available_segments.iter().map(|s| s.width).sum();
7060 if let Some(msgs) = debug_messages {
7061 msgs.push(LayoutDebugMessage::info(format!(
7062 "Final segments: {:?}, total available width: {}",
7063 available_segments, total_width
7064 )));
7065 msgs.push(LayoutDebugMessage::info(
7066 "--- Exiting get_line_constraints ---".to_string(),
7067 ));
7068 }
7069
7070 LineConstraints {
7071 segments: available_segments,
7072 total_available: total_width,
7073 }
7074}
7075
7076fn get_shape_horizontal_spans(
7079 shape: &ShapeBoundary,
7080 y: f32,
7081 line_height: f32,
7082) -> Result<Vec<(f32, f32)>, LayoutError> {
7083 match shape {
7084 ShapeBoundary::Rectangle(rect) => {
7085 let line_start = y;
7088 let line_end = y + line_height;
7089 let rect_start = rect.y;
7090 let rect_end = rect.y + rect.height;
7091
7092 if line_start < rect_end && line_end > rect_start {
7093 Ok(vec![(rect.x, rect.x + rect.width)])
7094 } else {
7095 Ok(vec![])
7096 }
7097 }
7098 ShapeBoundary::Circle { center, radius } => {
7099 let line_center_y = y + line_height / 2.0;
7100 let dy = (line_center_y - center.y).abs();
7101 if dy <= *radius {
7102 let dx = (radius.powi(2) - dy.powi(2)).sqrt();
7103 Ok(vec![(center.x - dx, center.x + dx)])
7104 } else {
7105 Ok(vec![])
7106 }
7107 }
7108 ShapeBoundary::Ellipse { center, radii } => {
7109 let line_center_y = y + line_height / 2.0;
7110 let dy = line_center_y - center.y;
7111 if dy.abs() <= radii.height {
7112 let y_term = dy / radii.height;
7114 let x_term_squared = 1.0 - y_term.powi(2);
7115 if x_term_squared >= 0.0 {
7116 let dx = radii.width * x_term_squared.sqrt();
7117 Ok(vec![(center.x - dx, center.x + dx)])
7118 } else {
7119 Ok(vec![])
7120 }
7121 } else {
7122 Ok(vec![])
7123 }
7124 }
7125 ShapeBoundary::Polygon { points } => {
7126 let segments = polygon_line_intersection(points, y, line_height)?;
7127 Ok(segments
7128 .iter()
7129 .map(|s| (s.start_x, s.start_x + s.width))
7130 .collect())
7131 }
7132 ShapeBoundary::Path { .. } => Ok(vec![]), }
7134}
7135
7136fn merge_segments(mut segments: Vec<LineSegment>) -> Vec<LineSegment> {
7138 if segments.len() <= 1 {
7139 return segments;
7140 }
7141 segments.sort_by(|a, b| a.start_x.partial_cmp(&b.start_x).unwrap());
7142 let mut merged = vec![segments[0].clone()];
7143 for next_seg in segments.iter().skip(1) {
7144 let last = merged.last_mut().unwrap();
7145 if next_seg.start_x <= last.start_x + last.width {
7146 let new_width = (next_seg.start_x + next_seg.width) - last.start_x;
7147 last.width = last.width.max(new_width);
7148 } else {
7149 merged.push(next_seg.clone());
7150 }
7151 }
7152 merged
7153}
7154
7155fn polygon_line_intersection(
7157 points: &[Point],
7158 y: f32,
7159 line_height: f32,
7160) -> Result<Vec<LineSegment>, LayoutError> {
7161 if points.len() < 3 {
7162 return Ok(vec![]);
7163 }
7164
7165 let line_center_y = y + line_height / 2.0;
7166 let mut intersections = Vec::new();
7167
7168 for i in 0..points.len() {
7170 let p1 = points[i];
7171 let p2 = points[(i + 1) % points.len()];
7172
7173 if (p2.y - p1.y).abs() < f32::EPSILON {
7175 continue;
7176 }
7177
7178 let crosses = (p1.y <= line_center_y && p2.y > line_center_y)
7180 || (p1.y > line_center_y && p2.y <= line_center_y);
7181
7182 if crosses {
7183 let t = (line_center_y - p1.y) / (p2.y - p1.y);
7185 let x = p1.x + t * (p2.x - p1.x);
7186 intersections.push(x);
7187 }
7188 }
7189
7190 intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
7192
7193 let mut segments = Vec::new();
7195 for chunk in intersections.chunks_exact(2) {
7196 let start_x = chunk[0];
7197 let end_x = chunk[1];
7198 if end_x > start_x {
7199 segments.push(LineSegment {
7200 start_x,
7201 width: end_x - start_x,
7202 priority: 0,
7203 });
7204 }
7205 }
7206
7207 Ok(segments)
7208}
7209
7210#[cfg(feature = "text_layout_hyphenation")]
7214fn get_hyphenator(language: HyphenationLanguage) -> Result<Standard, LayoutError> {
7215 Standard::from_embedded(language).map_err(|e| LayoutError::HyphenationError(e.to_string()))
7216}
7217
7218#[cfg(not(feature = "text_layout_hyphenation"))]
7220fn get_hyphenator(_language: Language) -> Result<Standard, LayoutError> {
7221 Err(LayoutError::HyphenationError("Hyphenation feature not enabled".to_string()))
7222}
7223
7224fn is_break_opportunity(item: &ShapedItem) -> bool {
7225 if is_word_separator(item) {
7227 return true;
7228 }
7229 if let ShapedItem::Break { .. } = item {
7230 return true;
7231 }
7232 if let ShapedItem::Cluster(c) = item {
7234 if c.text.starts_with('\u{00AD}') {
7235 return true;
7236 }
7237 }
7238 false
7239}
7240
7241pub struct BreakCursor<'a> {
7244 pub items: &'a [ShapedItem],
7246 pub next_item_index: usize,
7248 pub partial_remainder: Vec<ShapedItem>,
7251}
7252
7253impl<'a> BreakCursor<'a> {
7254 pub fn new(items: &'a [ShapedItem]) -> Self {
7255 Self {
7256 items,
7257 next_item_index: 0,
7258 partial_remainder: Vec::new(),
7259 }
7260 }
7261
7262 pub fn is_at_start(&self) -> bool {
7264 self.next_item_index == 0 && self.partial_remainder.is_empty()
7265 }
7266
7267 pub fn drain_remaining(&mut self) -> Vec<ShapedItem> {
7269 let mut remaining = std::mem::take(&mut self.partial_remainder);
7270 if self.next_item_index < self.items.len() {
7271 remaining.extend_from_slice(&self.items[self.next_item_index..]);
7272 }
7273 self.next_item_index = self.items.len();
7274 remaining
7275 }
7276
7277 pub fn is_done(&self) -> bool {
7279 self.next_item_index >= self.items.len() && self.partial_remainder.is_empty()
7280 }
7281
7282 pub fn consume(&mut self, count: usize) {
7284 if count == 0 {
7285 return;
7286 }
7287
7288 let remainder_len = self.partial_remainder.len();
7289 if count <= remainder_len {
7290 self.partial_remainder.drain(..count);
7292 } else {
7293 let from_main_list = count - remainder_len;
7295 self.partial_remainder.clear();
7296 self.next_item_index += from_main_list;
7297 }
7298 }
7299
7300 pub fn peek_next_unit(&self) -> Vec<ShapedItem> {
7304 let mut unit = Vec::new();
7305 let mut source_items = self.partial_remainder.clone();
7306 source_items.extend_from_slice(&self.items[self.next_item_index..]);
7307
7308 if source_items.is_empty() {
7309 return unit;
7310 }
7311
7312 if is_break_opportunity(&source_items[0]) {
7314 unit.push(source_items[0].clone());
7315 return unit;
7316 }
7317
7318 for item in source_items {
7320 if is_break_opportunity(&item) {
7321 break;
7322 }
7323 unit.push(item.clone());
7324 }
7325 unit
7326 }
7327}
7328
7329struct HyphenationResult {
7331 line_part: Vec<ShapedItem>,
7333 remainder_part: Vec<ShapedItem>,
7335}
7336
7337fn perform_bidi_analysis<'a, 'b: 'a>(
7338 styled_runs: &'a [TextRunInfo],
7339 full_text: &'b str,
7340 force_lang: Option<Language>,
7341) -> Result<(Vec<VisualRun<'a>>, BidiDirection), LayoutError> {
7342 if full_text.is_empty() {
7343 return Ok((Vec::new(), BidiDirection::Ltr));
7344 }
7345
7346 let bidi_info = BidiInfo::new(full_text, None);
7347 let para = &bidi_info.paragraphs[0];
7348 let base_direction = if para.level.is_rtl() {
7349 BidiDirection::Rtl
7350 } else {
7351 BidiDirection::Ltr
7352 };
7353
7354 let mut byte_to_run_index: Vec<usize> = vec![0; full_text.len()];
7356 for (run_idx, run) in styled_runs.iter().enumerate() {
7357 let start = run.logical_start;
7358 let end = start + run.text.len();
7359 for i in start..end {
7360 byte_to_run_index[i] = run_idx;
7361 }
7362 }
7363
7364 let mut final_visual_runs = Vec::new();
7365 let (levels, visual_run_ranges) = bidi_info.visual_runs(para, para.range.clone());
7366
7367 for range in visual_run_ranges {
7368 let bidi_level = levels[range.start];
7369 let mut sub_run_start = range.start;
7370
7371 for i in (range.start + 1)..range.end {
7373 if byte_to_run_index[i] != byte_to_run_index[sub_run_start] {
7374 let original_run_idx = byte_to_run_index[sub_run_start];
7376 let script = crate::text3::script::detect_script(&full_text[sub_run_start..i])
7377 .unwrap_or(Script::Latin);
7378 final_visual_runs.push(VisualRun {
7379 text_slice: &full_text[sub_run_start..i],
7380 style: styled_runs[original_run_idx].style.clone(),
7381 logical_start_byte: sub_run_start,
7382 bidi_level: BidiLevel::new(bidi_level.number()),
7383 language: force_lang.unwrap_or_else(|| {
7384 crate::text3::script::script_to_language(
7385 script,
7386 &full_text[sub_run_start..i],
7387 )
7388 }),
7389 script,
7390 });
7391 sub_run_start = i;
7393 }
7394 }
7395
7396 let original_run_idx = byte_to_run_index[sub_run_start];
7398 let script = crate::text3::script::detect_script(&full_text[sub_run_start..range.end])
7399 .unwrap_or(Script::Latin);
7400
7401 final_visual_runs.push(VisualRun {
7402 text_slice: &full_text[sub_run_start..range.end],
7403 style: styled_runs[original_run_idx].style.clone(),
7404 logical_start_byte: sub_run_start,
7405 bidi_level: BidiLevel::new(bidi_level.number()),
7406 script,
7407 language: force_lang.unwrap_or_else(|| {
7408 crate::text3::script::script_to_language(
7409 script,
7410 &full_text[sub_run_start..range.end],
7411 )
7412 }),
7413 });
7414 }
7415
7416 Ok((final_visual_runs, base_direction))
7417}
7418
7419fn get_justification_priority(class: CharacterClass) -> u8 {
7420 match class {
7421 CharacterClass::Space => 0,
7422 CharacterClass::Punctuation => 64,
7423 CharacterClass::Ideograph => 128,
7424 CharacterClass::Letter => 192,
7425 CharacterClass::Symbol => 224,
7426 CharacterClass::Combining => 255,
7427 }
7428}