use std::{
collections::HashMap,
num::NonZeroUsize,
sync::{Arc, Mutex},
};
use azul_css::props::basic::ColorU;
use hyphenation::{Language, Load, Standard};
use rust_fontconfig::{FcWeight, FontId};
use super::{create_mock_font_manager, default_style, MockFont};
use crate::{
font::parsed::ParsedFont,
text3::{cache::*, default::PathLoader, script::Script},
};
#[test]
fn test_logical_items_combine_upright() {
let mut style = (*default_style()).clone();
style.text_combine_upright = Some(TextCombineUpright::Digits(2));
let content = vec![InlineContent::Text(StyledRun {
text: "12ab345c".into(),
style: Arc::new(style),
logical_start_byte: 0,
})];
let logical_items = create_logical_items(&content, &[]);
assert_eq!(logical_items.len(), 5);
let content = vec![InlineContent::Text(StyledRun {
text: "12ab 345c".into(),
style: default_style(),
logical_start_byte: 0,
})];
let mut partial_style = PartialStyleProperties::default();
partial_style.text_combine_upright = Some(Some(TextCombineUpright::Digits(2)));
let overrides = vec![
StyleOverride {
target: ContentIndex {
run_index: 0,
item_index: 0,
},
style: partial_style.clone(),
},
StyleOverride {
target: ContentIndex {
run_index: 0,
item_index: 5,
},
style: partial_style.clone(),
},
];
let logical_items = create_logical_items(&content, &overrides);
assert_eq!(logical_items.len(), 4);
match &logical_items[0] {
LogicalItem::CombinedText { text, .. } => assert_eq!(text, "12"),
other => panic!("Expected CombinedText, got {:?}", other),
}
match &logical_items[1] {
LogicalItem::Text { text, .. } => assert_eq!(text, "ab "),
other => panic!("Expected Text, got {:?}", other),
}
match &logical_items[2] {
LogicalItem::CombinedText { text, .. } => assert_eq!(text, "34"),
other => panic!("Expected CombinedText, got {:?}", other),
}
match &logical_items[3] {
LogicalItem::Text { text, .. } => assert_eq!(text, "5c"),
other => panic!("Expected Text, got {:?}", other),
}
}
#[test]
fn test_bidi_reordering_mixed_content() {
let content = vec![
InlineContent::Text(StyledRun {
text: "hello ".into(),
style: default_style(),
logical_start_byte: 0,
}),
InlineContent::Text(StyledRun {
text: "שלום".into(), style: default_style(),
logical_start_byte: 6,
}),
InlineContent::Text(StyledRun {
text: " world".into(),
style: default_style(),
logical_start_byte: 14, }),
];
let logical_items = create_logical_items(&content, &[]);
let visual_items = reorder_logical_items(&logical_items, Direction::Ltr).unwrap();
assert_eq!(visual_items.len(), 3);
assert_eq!(visual_items[0].text, "hello ");
assert_eq!(visual_items[0].bidi_level.level(), 0); assert_eq!(visual_items[1].text, "שלום");
assert_eq!(visual_items[1].bidi_level.level(), 1); assert_eq!(visual_items[2].text, " world");
assert_eq!(visual_items[2].bidi_level.level(), 0); }
#[test]
fn test_long_word_overflow_no_hyphenation() {
let manager = create_mock_font_manager();
let text = "supercalifragilisticexpialidocious"; let content = vec![InlineContent::Text(StyledRun {
text: text.into(),
style: default_style(),
logical_start_byte: 0,
})];
let constraints = UnifiedConstraints {
available_width: 100.0, ..Default::default()
};
let logical_items = create_logical_items(&content, &[]);
let visual_items = reorder_logical_items(&logical_items, Direction::Ltr).unwrap();
let shaped_items = shape_visual_items(&visual_items, &manager).unwrap();
let mut cursor = BreakCursor::new(&shaped_items);
let (line_items, _) = break_one_line(
&mut cursor,
&LineConstraints {
segments: vec![LineSegment {
start_x: 0.0,
width: 100.0,
priority: 0,
}],
total_available: 100.0,
},
false,
None,
);
assert!(
!line_items.is_empty(),
"Line should not be empty to prevent infinite loop"
);
}
#[test]
fn test_multi_column_layout() {
let manager = create_mock_font_manager();
let content = vec![InlineContent::Text(StyledRun {
text: "a b c d e f g h".into(),
style: default_style(),
logical_start_byte: 0,
})];
let constraints = UnifiedConstraints {
available_width: 100.0,
available_height: Some(25.0), columns: 2,
column_gap: 10.0,
..Default::default()
};
let logical_items = create_logical_items(&content, &[]);
let visual_items = reorder_logical_items(&logical_items, Direction::Ltr).unwrap();
let shaped_items = shape_visual_items(&visual_items, &manager).unwrap();
let mut cursor = BreakCursor::new(&shaped_items);
let layout = perform_fragment_layout(&mut cursor, &logical_items, &constraints).unwrap();
let mut col1_items = 0;
let mut col2_items = 0;
let col2_start_x = 45.0 + 10.0;
for item in &layout.items {
if item.position.x < col2_start_x {
col1_items += 1;
assert!(item.position.x >= 0.0 && item.position.x < 45.0);
} else {
col2_items += 1;
assert!(item.position.x >= col2_start_x && item.position.x < 100.0);
}
}
let line_1_col_1 = layout
.items
.iter()
.filter(|i| i.line_index == 0 && i.position.x < col2_start_x)
.count();
let line_2_col_1 = layout
.items
.iter()
.filter(|i| i.line_index == 1 && i.position.x < col2_start_x)
.count();
let line_1_col_2 = layout
.items
.iter()
.filter(|i| i.line_index == 0 && i.position.x >= col2_start_x)
.count();
assert!(col1_items > 0);
assert!(col2_items > 0);
}
#[test]
fn test_line_clamp() {
let manager = create_mock_font_manager();
let content = vec![InlineContent::Text(StyledRun {
text: "a a a a a a a a a a".into(),
style: default_style(),
logical_start_byte: 0,
})];
let constraints = UnifiedConstraints {
available_width: 30.0, line_clamp: NonZeroUsize::new(2),
..Default::default()
};
let logical_items = create_logical_items(&content, &[]);
let visual_items = reorder_logical_items(&logical_items, Direction::Ltr).unwrap();
let shaped_items = shape_visual_items(&visual_items, &manager).unwrap();
let mut cursor = BreakCursor::new(&shaped_items);
let layout = perform_fragment_layout(&mut cursor, &logical_items, &constraints).unwrap();
let max_line_index = layout.items.iter().map(|i| i.line_index).max().unwrap_or(0);
assert_eq!(
max_line_index, 1,
"Layout should be clamped to 2 lines (index 0 and 1)"
);
assert!(
!cursor.is_done(),
"Cursor should have remaining items after clamping"
);
}
#[test]
fn test_flow_across_fragments() {
let mut cache = LayoutCache::new();
let manager = create_mock_font_manager();
let content = vec![InlineContent::Text(StyledRun {
text: "line one and line two and line three".into(),
style: default_style(),
logical_start_byte: 0,
})];
let flow_chain = vec![
LayoutFragment {
id: "frag1".into(),
constraints: UnifiedConstraints {
available_width: 100.0,
available_height: Some(15.0), ..Default::default()
},
},
LayoutFragment {
id: "frag2".into(),
constraints: UnifiedConstraints {
available_width: 100.0,
available_height: Some(30.0), ..Default::default()
},
},
];
let result = cache
.layout_flow(&content, &[], &flow_chain, &manager)
.unwrap();
let frag1_layout = result.fragment_layouts.get("frag1").unwrap();
let frag2_layout = result.fragment_layouts.get("frag2").unwrap();
assert!(!frag1_layout.items.is_empty());
assert!(!frag2_layout.items.is_empty());
let frag1_max_line = frag1_layout
.items
.iter()
.map(|i| i.line_index)
.max()
.unwrap_or(0);
assert_eq!(frag1_max_line, 0, "Fragment 1 should only contain one line");
let frag2_max_line = frag2_layout
.items
.iter()
.map(|i| i.line_index)
.max()
.unwrap_or(0);
assert!(
frag2_max_line > 0,
"Fragment 2 should contain subsequent lines"
);
assert!(result.remaining_items.is_empty());
}
#[test]
fn test_kashida_justification() {
let manager = create_mock_font_manager();
let content = vec![InlineContent::Text(StyledRun {
text: "مرحبا".into(),
style: default_style(),
logical_start_byte: 0,
})];
let constraints = UnifiedConstraints {
available_width: 100.0,
text_justify: JustifyContent::Kashida,
text_align: TextAlign::Justify,
..Default::default()
};
let logical_items = create_logical_items(&content, &[]);
let visual_items = reorder_logical_items(&logical_items, Direction::Rtl).unwrap();
let shaped_items = shape_visual_items(&visual_items, &manager).unwrap();
let line_constraints = LineConstraints {
segments: vec![LineSegment {
start_x: 0.0,
width: 100.0,
priority: 0,
}],
total_available: 100.0,
};
let justified_items = justify_kashida_and_rebuild(shaped_items, &line_constraints, false);
let kashida_count = justified_items.iter().filter(|item| {
matches!(item, ShapedItem::Cluster(c) if c.glyphs.iter().any(|g| matches!(g.kind, GlyphKind::Kashida {..})))
}).count();
assert_eq!(kashida_count, 6, "Expected 6 kashida glyphs to be inserted");
let new_width: f32 = justified_items
.iter()
.map(|i| get_item_measure(i, false))
.sum();
assert!((new_width - 97.0).abs() < 1e-5);
}
#[test]
fn test_layout_with_shape_exclusion() {
let manager = create_mock_font_manager();
let content = vec![InlineContent::Text(StyledRun {
text: "this is some very long text that should wrap around a floated exclusion area in \
the middle"
.into(),
style: default_style(),
logical_start_byte: 0,
})];
let constraints = UnifiedConstraints {
available_width: 300.0,
available_height: Some(100.0),
line_height: 16.0, shape_exclusions: vec![ShapeBoundary::Rectangle(Rect {
x: 100.0,
y: 10.0,
width: 100.0,
height: 30.0,
})],
..Default::default()
};
let is_line_split = |items: &Vec<&PositionedItem<MockFont>>| -> bool {
if items.is_empty() {
return false;
}
let has_left_part = items.iter().any(|i| i.position.x < 100.0);
let has_right_part = items.iter().any(|i| i.position.x >= 200.0);
let no_middle_part = !items
.iter()
.any(|i| i.position.x >= 100.0 && i.position.x < 200.0);
has_left_part && has_right_part && no_middle_part
};
let logical_items = create_logical_items(&content, &[]);
let visual_items = reorder_logical_items(&logical_items, Direction::Ltr).unwrap();
let shaped_items = shape_visual_items(&visual_items, &manager).unwrap();
let mut cursor = BreakCursor::new(&shaped_items);
let layout = perform_fragment_layout(&mut cursor, &logical_items, &constraints).unwrap();
let line0_items: Vec<_> = layout.items.iter().filter(|i| i.line_index == 0).collect();
let line1_items: Vec<_> = layout.items.iter().filter(|i| i.line_index == 1).collect();
let line2_items: Vec<_> = layout.items.iter().filter(|i| i.line_index == 2).collect();
let line3_items: Vec<_> = layout.items.iter().filter(|i| i.line_index == 3).collect();
assert!(is_line_split(&line0_items), "Line 0 (y=0) should be split");
assert!(is_line_split(&line1_items), "Line 1 (y=16) should be split");
assert!(is_line_split(&line2_items), "Line 2 (y=32) should be split");
assert!(
!is_line_split(&line3_items),
"Line 3 (y=48) should not be split"
);
}
#[test]
fn test_bug1_shaping_across_style_boundaries() {
let content = vec![InlineContent::Text(StyledRun {
text: "first fish".into(),
style: default_style(),
logical_start_byte: 0,
})];
let overrides = vec![StyleOverride {
target: ContentIndex {
run_index: 0,
item_index: 1,
}, style: PartialStyleProperties {
color: Some(ColorU {
r: 255,
g: 0,
b: 0,
a: 255,
}),
..Default::default()
},
}];
let logical_items = create_logical_items(&content, &overrides);
assert_eq!(logical_items.len(), 3);
match &logical_items[0] {
LogicalItem::Text { text, .. } => assert_eq!(text, "f"),
_ => panic!("Expected text"),
}
match &logical_items[1] {
LogicalItem::Text { text, .. } => assert_eq!(text, "i"),
_ => panic!("Expected text"),
}
match &logical_items[2] {
LogicalItem::Text { text, .. } => assert_eq!(text, "rst fish"),
_ => panic!("Expected text"),
}
}
#[test]
fn test_bug3_rtl_glyph_reversal() {
let mut cache = LayoutCache::<MockFont>::new();
let manager = create_mock_font_manager();
let text = "\u{05e9}\u{05dc}\u{05d5}\u{05dd}";
let style = default_style();
let visual_items = vec![VisualItem {
logical_source: LogicalItem::Text {
source: ContentIndex {
run_index: 0,
item_index: 0,
},
text: text.to_string(),
style: style.clone(),
},
bidi_level: BidiLevel::new(1), script: Script::Hebrew,
text: text.to_string(),
}];
let shaped_items = shape_visual_items(&visual_items, &manager).unwrap();
assert_eq!(shaped_items.len(), 4);
let constraints = UnifiedConstraints {
available_width: 200.0,
..Default::default()
};
let mut cursor = BreakCursor::new(&shaped_items);
let logical_items = create_logical_items(
&[InlineContent::Text(StyledRun {
text: text.to_string(),
style,
logical_start_byte: 0,
})],
&[],
);
let layout = perform_fragment_layout(&mut cursor, &logical_items, &constraints).unwrap();
assert_eq!(layout.items.len(), 4);
let pos0 = layout.items[0].position.x; let pos1 = layout.items[1].position.x; let pos2 = layout.items[2].position.x; let pos3 = layout.items[3].position.x;
assert!(pos1 > pos0);
assert!(pos2 > pos1);
assert!(pos3 > pos2);
}
#[test]
fn test_simple_line_break() {
let manager = create_mock_font_manager();
let content = vec![InlineContent::Text(StyledRun {
text: "a a a a a a".into(), style: default_style(),
logical_start_byte: 0,
})];
let flow_chain = vec![LayoutFragment {
id: "main".into(),
constraints: UnifiedConstraints {
available_width: 50.0,
..Default::default()
},
}];
let logical_items = create_logical_items(&content, &[]);
let visual_items = reorder_logical_items(&logical_items, Direction::Ltr).unwrap();
let shaped_items = shape_visual_items(&visual_items, &manager).unwrap();
let mut cursor = BreakCursor::new(&shaped_items);
let layout =
perform_fragment_layout(&mut cursor, &logical_items, &flow_chain[0].constraints).unwrap();
let line1_items_count = layout.items.iter().filter(|i| i.line_index == 0).count();
let line2_items_count = layout.items.iter().filter(|i| i.line_index == 1).count();
assert_eq!(
line1_items_count, 7,
"Line 1 should have 7 items ('a a a a')"
);
assert_eq!(line2_items_count, 4, "Line 2 should have 4 items (' a a')");
}
#[test]
fn test_justification_inter_word() {
let manager = create_mock_font_manager();
let content = vec![InlineContent::Text(StyledRun {
text: "a b".into(), style: default_style(),
logical_start_byte: 0,
})];
let constraints = UnifiedConstraints {
available_width: 100.0,
text_justify: JustifyContent::InterWord,
text_align: TextAlign::JustifyAll,
..Default::default()
};
let logical_items = create_logical_items(&content, &[]);
let visual_items = reorder_logical_items(&logical_items, Direction::Ltr).unwrap();
let shaped_items = shape_visual_items(&visual_items, &manager).unwrap();
let mut cursor = BreakCursor::new(&shaped_items);
let layout = perform_fragment_layout(&mut cursor, &logical_items, &constraints).unwrap();
assert_eq!(layout.items.len(), 3);
let pos_a = layout.items[0].position.x;
let pos_space = layout.items[1].position.x;
let pos_b = layout.items[2].position.x;
assert_eq!(pos_a, 0.0);
assert_eq!(pos_space, 8.0);
assert!((pos_b - 91.0).abs() < 1e-5);
}
#[test]
fn test_hyphenation_break() {
let manager = create_mock_font_manager();
let hyphenator = Standard::from_embedded(Language::EnglishUS).unwrap();
let text = "breaking";
let content = vec![InlineContent::Text(StyledRun {
text: text.into(),
style: Arc::new(StyleProperties {
font_size_px: 10.0,
..(*default_style()).clone()
}),
logical_start_byte: 0,
})];
let shaped_items = shape_visual_items(
&reorder_logical_items(&create_logical_items(&content, &[]), Direction::Ltr).unwrap(),
&manager,
)
.unwrap();
let mut cursor = BreakCursor::new(&shaped_items);
let line_constraints = LineConstraints {
segments: vec![LineSegment {
start_x: 0.0,
width: 50.0, priority: 0,
}],
total_available: 50.0,
};
let (line1_items, was_hyphenated) =
break_one_line(&mut cursor, &line_constraints, false, Some(&hyphenator));
assert!(was_hyphenated, "hyphenation should have occurred");
let last_item = line1_items.last().unwrap();
let is_hyphen = matches!(&last_item, ShapedItem::Cluster(c) if c.glyphs.iter().any(|g| g.kind == GlyphKind::Hyphen));
assert!(is_hyphen, "Last item was not a hyphen");
let remainder = cursor.drain_remaining();
let remainder_text: String = remainder
.iter()
.map(|item| {
if let ShapedItem::Cluster(c) = item {
c.text.as_str()
} else {
""
}
})
.collect();
assert_eq!(remainder_text, "ing");
}
#[test]
fn test_hyphenation_break_2() {
let manager = create_mock_font_manager();
let hyphenator = Standard::from_embedded(Language::EnglishUS).unwrap();
let text = "hyphenation";
let content = vec![InlineContent::Text(StyledRun {
text: text.into(),
style: Arc::new(StyleProperties {
font_size_px: 10.0,
..(*default_style()).clone()
}),
logical_start_byte: 0,
})];
let shaped_items = shape_visual_items(
&reorder_logical_items(&create_logical_items(&content, &[]), Direction::Ltr).unwrap(),
&manager,
)
.unwrap();
let mut cursor = BreakCursor::new(&shaped_items);
let line_constraints = LineConstraints {
segments: vec![LineSegment {
start_x: 0.0,
width: 60.0,
priority: 0,
}],
total_available: 60.0,
};
let (line1_items, was_hyphenated) =
break_one_line(&mut cursor, &line_constraints, false, Some(&hyphenator));
assert!(was_hyphenated, "hyphenation should have occurred");
let last_item = line1_items.last().unwrap();
let is_hyphen = matches!(&last_item, ShapedItem::Cluster(c) if c.glyphs.iter().any(|g| g.kind == GlyphKind::Hyphen));
assert!(is_hyphen, "Last item was not a hyphen");
let remainder = cursor.drain_remaining();
let remainder_text: String = remainder
.iter()
.map(|item| {
if let ShapedItem::Cluster(c) = item {
c.text.as_str()
} else {
""
}
})
.collect();
assert_eq!(remainder_text, "ation");
}
#[test]
fn test_empty_input_layout() {
let mut cache = LayoutCache::new();
let manager = create_mock_font_manager();
let content = vec![];
let flow_chain = vec![LayoutFragment {
id: "main".into(),
constraints: UnifiedConstraints {
available_width: 100.0,
..Default::default()
},
}];
let result = cache
.layout_flow(&content, &[], &flow_chain, &manager)
.unwrap();
let main_layout = result.fragment_layouts.get("main").unwrap();
assert!(main_layout.items.is_empty());
let main_bounds = main_layout.bounds();
assert_eq!(main_bounds.width, 0.0);
assert_eq!(main_bounds.height, 0.0);
assert!(result.remaining_items.is_empty());
}