StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use super::*; |
| 2 | use crate::fonts::Properties; |
| 3 | |
| 4 | use crate::fonts::{collect_glyph_indices, collect_line_caret_position_starts, init_fonts}; |
| 5 | use crate::platform::FontDB as _; |
| 6 | use crate::text_layout::DEFAULT_TOP_BOTTOM_RATIO; |
| 7 | |
| 8 | use anyhow::Result; |
| 9 | use rand::random; |
| 10 | |
| 11 | pub(crate) fn collect_line_caret_position_pairs(line: &Line) -> Vec<(usize, usize)> { |
| 12 | line.caret_positions |
| 13 | .iter() |
| 14 | .map(|pos| (pos.start_offset, pos.last_offset)) |
| 15 | .collect_vec() |
| 16 | } |
| 17 | |
| 18 | #[test] |
| 19 | fn test_char_indices_ligatures() -> Result<()> { |
| 20 | let mut font_db = FontDB::new(); |
| 21 | let zapfino = font_db.load_from_system("Zapfino")?; |
| 22 | let menlo = font_db.load_from_system("Menlo")?; |
| 23 | |
| 24 | let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈"; |
| 25 | let line = layout_line( |
| 26 | text, |
| 27 | LineStyle { |
| 28 | font_size: 16.0, |
| 29 | line_height_ratio: 1.2, |
| 30 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 31 | fixed_width_tab_size: None, |
| 32 | }, |
| 33 | &[ |
| 34 | ( |
| 35 | 0..9, |
| 36 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 37 | ), |
| 38 | ( |
| 39 | 9..22, |
| 40 | StyleAndFont::new(menlo, Properties::default(), TextStyle::new()), |
| 41 | ), |
| 42 | ( |
| 43 | 22..text.encode_utf16().count(), |
| 44 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 45 | ), |
| 46 | ], |
| 47 | &font_db, |
| 48 | ClipConfig::default(), |
| 49 | ); |
| 50 | |
| 51 | // It's easiest to understand what's happening here by visualizing the text and seeing which |
| 52 | // characters get combined to become a single glyph. At a high level, what this is testing |
| 53 | // is that after laying out the string, we see some characters get combined into a single |
| 54 | // glyph. For example, the text "Zapfino" gets combined into a single glyph, which is why |
| 55 | // there is a jump from 23 to 30 in the list of glyph indices below. |
| 56 | // See https://docs.google.com/drawings/d/18qOKhzA5rWaMuxKVeWFDXh7ebrDjxongarAckkm0qnE/edit |
| 57 | // for a full diagram of what's happening here. |
| 58 | assert_eq!( |
| 59 | line.runs |
| 60 | .iter() |
| 61 | .flat_map(|r| r.glyphs.iter()) |
| 62 | .map(|g| g.index) |
| 63 | .collect::<Vec<_>>(), |
| 64 | vec![0, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 30, 31] |
| 65 | ); |
| 66 | Ok(()) |
| 67 | } |
| 68 | |
| 69 | #[test] |
| 70 | fn test_caret_positions_ligatures() -> Result<()> { |
| 71 | // There's some overlap between caret positions and the character indices we |
| 72 | // store in glyphs. However, a single glyph may have multiple caret positions |
| 73 | // because characters/graphemes may get combined into a single glyph. |
| 74 | |
| 75 | let mut font_db = FontDB::new(); |
| 76 | let zapfino = font_db.load_from_system("Zapfino")?; |
| 77 | let menlo = font_db.load_from_system("Menlo")?; |
| 78 | |
| 79 | // This string has 32 characters, but 35 UTF-16 code points and 41 UTF-8 code points. |
| 80 | // Each '𐍈' character encodes as 2 UTF-16 code points or 4 UTF-8 code points. |
| 81 | let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈"; |
| 82 | |
| 83 | let line = layout_line( |
| 84 | text, |
| 85 | LineStyle { |
| 86 | font_size: 16.0, |
| 87 | line_height_ratio: 1.2, |
| 88 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 89 | fixed_width_tab_size: None, |
| 90 | }, |
| 91 | &[ |
| 92 | ( |
| 93 | 0..9, |
| 94 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 95 | ), |
| 96 | ( |
| 97 | 9..22, |
| 98 | StyleAndFont::new(menlo, Properties::default(), TextStyle::new()), |
| 99 | ), |
| 100 | ( |
| 101 | 22..35, |
| 102 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 103 | ), |
| 104 | ], |
| 105 | &font_db, |
| 106 | ClipConfig::default(), |
| 107 | ); |
| 108 | |
| 109 | // There are only 23 glyphs because 'Zapfino', 'Th', and 'is' each have ligatures. |
| 110 | assert_eq!( |
| 111 | line.runs.iter().map(|run| run.glyphs.len()).sum::<usize>(), |
| 112 | 23 |
| 113 | ); |
| 114 | |
| 115 | // There should be a caret position for each character. |
| 116 | assert_eq!( |
| 117 | line.caret_positions |
| 118 | .iter() |
| 119 | .map(|pos| pos.start_offset) |
| 120 | .collect::<Vec<_>>(), |
| 121 | (0..32).collect::<Vec<usize>>() |
| 122 | ); |
| 123 | |
| 124 | // There is a caret for the 3rd character at the 3rd position, even though |
| 125 | // the first 2 characters are represented with 1 glyph. |
| 126 | assert_eq!( |
| 127 | line.caret_position_for_index(3), |
| 128 | line.caret_positions[3].position_in_line |
| 129 | ); |
| 130 | |
| 131 | // Likewise for the second 𐍈, even though it (and the previous one) take |
| 132 | // multiple code points. |
| 133 | assert_eq!( |
| 134 | line.caret_position_for_index(15), |
| 135 | line.caret_positions[15].position_in_line |
| 136 | ); |
| 137 | |
| 138 | // This tests hit-testing on a regular character. |
| 139 | assert_eq!(line.caret_index_for_x(0.), Some(0)); |
| 140 | assert_eq!(line.caret_index_for_x(20.), Some(1)); |
| 141 | |
| 142 | // This tests hit-testing within a ligature. |
| 143 | assert_eq!(line.caret_index_for_x(260.), Some(25)); |
| 144 | |
| 145 | // This tests rounding up to the next character. |
| 146 | assert_eq!(line.caret_index_for_x(268.), Some(26)); |
| 147 | |
| 148 | // This tests a few random positions within the bound and before the last character. |
| 149 | let last_caret_pos = line |
| 150 | .caret_positions |
| 151 | .last() |
| 152 | .map_or(0., |p| p.position_in_line); |
| 153 | // The bounded and unbounded method should return the same result. |
| 154 | for _ in 0..5 { |
| 155 | let pos: f32 = random(); |
| 156 | let index = line.caret_index_for_x(pos * last_caret_pos); |
| 157 | assert_eq!( |
| 158 | index, |
| 159 | Some(line.caret_index_for_x_unbounded(pos * last_caret_pos)) |
| 160 | ); |
| 161 | } |
| 162 | |
| 163 | // This tests that the unbounded method returns the first index for out-of-bound position to the left |
| 164 | assert_eq!(line.caret_index_for_x_unbounded(-1.), line.first_index()); |
| 165 | // The bounded method should return `None` |
| 166 | assert_eq!(line.caret_index_for_x(-1.), None); |
| 167 | |
| 168 | // This tests that the unbounded method returns the end index for out-of-bound position to the right |
| 169 | assert_eq!( |
| 170 | line.caret_index_for_x_unbounded(line.width + 0.1), |
| 171 | line.end_index() |
| 172 | ); |
| 173 | assert_eq!(line.caret_index_for_x(line.width + 0.1), None); |
| 174 | |
| 175 | // This tests that the unbounded method returns the correct index either before or after the last glyph |
| 176 | assert_eq!( |
| 177 | line.caret_index_for_x_unbounded(0.9 * last_caret_pos + 0.1 * line.width), |
| 178 | line.last_index() |
| 179 | ); |
| 180 | assert_eq!( |
| 181 | line.caret_index_for_x_unbounded(0.1 * last_caret_pos + 0.9 * line.width), |
| 182 | line.end_index() |
| 183 | ); |
| 184 | // The bounded method should always just return the last index |
| 185 | assert_eq!( |
| 186 | line.caret_index_for_x(0.9 * last_caret_pos + 0.1 * line.width), |
| 187 | Some(line.last_index()) |
| 188 | ); |
| 189 | assert_eq!( |
| 190 | line.caret_index_for_x(0.1 * last_caret_pos + 0.9 * line.width), |
| 191 | Some(line.last_index()) |
| 192 | ); |
| 193 | |
| 194 | Ok(()) |
| 195 | } |
| 196 | |
| 197 | /// The emojis in this test use font fallback, which means it won't behave |
| 198 | /// consistently across platforms. |
| 199 | #[test] |
| 200 | fn test_emoji_caret_positions() -> Result<()> { |
| 201 | let (font_db, font_family) = init_fonts(); |
| 202 | |
| 203 | // We're using these emoji specifically because they're represented as multiple |
| 204 | // combined characters. |
| 205 | let text = "👨👧👧🇨🇦"; |
| 206 | |
| 207 | let line = font_db.text_layout_system().layout_line( |
| 208 | text, |
| 209 | LineStyle { |
| 210 | font_size: 16.0, |
| 211 | line_height_ratio: 1.2, |
| 212 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 213 | fixed_width_tab_size: None, |
| 214 | }, |
| 215 | &[( |
| 216 | 0..12, |
| 217 | StyleAndFont::new(font_family, Properties::default(), TextStyle::new()), |
| 218 | )], |
| 219 | 10000.0, |
| 220 | ClipConfig::default(), |
| 221 | ); |
| 222 | |
| 223 | // We want the leading edge for caret positions, so the first one is at the |
| 224 | // start of the line. |
| 225 | assert_eq!(line.caret_positions[0].position_in_line, 0.0); |
| 226 | |
| 227 | assert_eq!( |
| 228 | collect_line_caret_position_starts(&line), |
| 229 | // CoreText gives us one caret position per visible character. |
| 230 | // Each emoji is multiple characters, but one grapheme and therefore one |
| 231 | // caret position. |
| 232 | vec![0, 5] |
| 233 | ); |
| 234 | |
| 235 | // The first character is within the first emoji, so its caret position is |
| 236 | // at the start of the line. |
| 237 | assert_eq!(line.caret_position_for_index(0), 0.0); |
| 238 | // Likewise, the start of the next emoji returns its start position. |
| 239 | assert_eq!( |
| 240 | line.caret_position_for_index(5), |
| 241 | line.caret_positions[1].position_in_line |
| 242 | ); |
| 243 | // Subsequent positions within the emoji also resolve to its starting offset. |
| 244 | assert_eq!( |
| 245 | line.caret_position_for_index(6), |
| 246 | line.caret_positions[1].position_in_line |
| 247 | ); |
| 248 | // Past the end of the last emoji, we clamp to the end of the line. |
| 249 | assert_eq!(line.caret_position_for_index(7), line.width); |
| 250 | |
| 251 | Ok(()) |
| 252 | } |
| 253 | |
| 254 | /// The RTL text and emoji in this test use font fallback, which means |
| 255 | /// this test won't behave consistently across platforms. |
| 256 | #[test] |
| 257 | fn test_bidi_caret_positions() -> Result<()> { |
| 258 | let (font_db, font_family) = init_fonts(); |
| 259 | |
| 260 | let text = "a שָׁלוֹם 🇨🇦 test"; |
| 261 | let line = font_db.text_layout_system().layout_line( |
| 262 | text, |
| 263 | LineStyle { |
| 264 | font_size: 16.0, |
| 265 | line_height_ratio: 1.2, |
| 266 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 267 | fixed_width_tab_size: None, |
| 268 | }, |
| 269 | &[( |
| 270 | 0..text.encode_utf16().count(), |
| 271 | StyleAndFont::new(font_family, Properties::default(), TextStyle::new()), |
| 272 | )], |
| 273 | 10000.0, |
| 274 | ClipConfig::default(), |
| 275 | ); |
| 276 | |
| 277 | // Caret positions should account for diacritics in the Hebrew text, as well |
| 278 | // as the 🇨🇦 emoji consisting of multiple characters. In addition, they should |
| 279 | // be sorted by display order. |
| 280 | assert_eq!( |
| 281 | collect_line_caret_position_pairs(&line), |
| 282 | vec![ |
| 283 | // "a " |
| 284 | (0, 0), |
| 285 | (1, 1), |
| 286 | // שָׁלוֹם |
| 287 | (8, 8), |
| 288 | (6, 7), |
| 289 | (5, 5), |
| 290 | (2, 4), |
| 291 | // " " |
| 292 | (9, 9), |
| 293 | // 🇨🇦 |
| 294 | (10, 11), |
| 295 | // " test" |
| 296 | (12, 12), |
| 297 | (13, 13), |
| 298 | (14, 14), |
| 299 | (15, 15), |
| 300 | (16, 16) |
| 301 | ] |
| 302 | ); |
| 303 | |
| 304 | Ok(()) |
| 305 | } |
| 306 | |
| 307 | #[test] |
| 308 | fn test_layout_text_ligatures() -> Result<()> { |
| 309 | let mut font_db = FontDB::new(); |
| 310 | let zapfino = font_db.load_from_system("Zapfino")?; |
| 311 | let menlo = font_db.load_from_system("Menlo")?; |
| 312 | |
| 313 | let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈"; |
| 314 | let frame = layout_text( |
| 315 | text, |
| 316 | LineStyle { |
| 317 | font_size: 16.0, |
| 318 | line_height_ratio: 1.2, |
| 319 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 320 | fixed_width_tab_size: None, |
| 321 | }, |
| 322 | &[ |
| 323 | ( |
| 324 | 0..9, |
| 325 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 326 | ), |
| 327 | ( |
| 328 | 9..22, |
| 329 | StyleAndFont::new(menlo, Properties::default(), TextStyle::new()), |
| 330 | ), |
| 331 | ( |
| 332 | 22..text.encode_utf16().count(), |
| 333 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 334 | ), |
| 335 | ], |
| 336 | &font_db, |
| 337 | 125., /* max_width */ |
| 338 | f32::MAX, /* max_height */ |
| 339 | Default::default(), |
| 340 | None, |
| 341 | ); |
| 342 | |
| 343 | // The text should contain multiple lines since it can't fit in 125 pixels on the first |
| 344 | // line. |
| 345 | assert_eq!(frame.lines().len(), 4); |
| 346 | |
| 347 | // The text should be wrapped over 4 lines and look like this: |
| 348 | // "This is |
| 349 | // m𐍈re or |
| 350 | // less, |
| 351 | // Zapfino!𐍈" |
| 352 | assert_eq!( |
| 353 | collect_glyph_indices(&frame), |
| 354 | vec![ |
| 355 | vec![0, 2, 4, 5, 7, 8], |
| 356 | vec![9, 10, 11, 12, 13, 14, 15, 16], |
| 357 | vec![17, 18, 19, 20, 21, 22,], |
| 358 | vec![23, 30, 31] |
| 359 | ] |
| 360 | ); |
| 361 | |
| 362 | Ok(()) |
| 363 | } |
| 364 | |
| 365 | #[test] |
| 366 | fn test_layout_text_first_line_head_indent_ligatures() -> Result<()> { |
| 367 | // Similar test to above, except we add in a left head indent (with reduced max width)! |
| 368 | let mut font_db = FontDB::new(); |
| 369 | let zapfino = font_db.load_from_system("Zapfino")?; |
| 370 | let menlo = font_db.load_from_system("Menlo")?; |
| 371 | |
| 372 | let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈"; |
| 373 | let frame = layout_text( |
| 374 | text, |
| 375 | LineStyle { |
| 376 | font_size: 16.0, |
| 377 | line_height_ratio: 1.2, |
| 378 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 379 | fixed_width_tab_size: None, |
| 380 | }, |
| 381 | &[ |
| 382 | ( |
| 383 | 0..9, |
| 384 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 385 | ), |
| 386 | ( |
| 387 | 9..22, |
| 388 | StyleAndFont::new(menlo, Properties::default(), TextStyle::new()), |
| 389 | ), |
| 390 | ( |
| 391 | 22..text.encode_utf16().count(), |
| 392 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 393 | ), |
| 394 | ], |
| 395 | &font_db, |
| 396 | 80., /* max_width */ |
| 397 | f32::MAX, /* max_height */ |
| 398 | Default::default(), |
| 399 | Some(50.), /* first_line_head_indent */ |
| 400 | ); |
| 401 | |
| 402 | // The text should contain multiple lines since we have a 50px left head indent on the first |
| 403 | // line and then each line only has 80px. |
| 404 | assert_eq!(frame.lines().len(), 6); |
| 405 | |
| 406 | assert_eq!( |
| 407 | collect_glyph_indices(&frame), |
| 408 | vec![ |
| 409 | vec![0], // left head indent means we don't have much content laid out on this line. |
| 410 | vec![1, 2, 4, 5, 7, 8], |
| 411 | vec![9, 10, 11, 12, 13, 14, 15, 16], |
| 412 | vec![17, 18, 19, 20, 21, 22], |
| 413 | vec![23, 24, 25, 27, 28], |
| 414 | vec![29, 30, 31], |
| 415 | ] |
| 416 | ); |
| 417 | |
| 418 | Ok(()) |
| 419 | } |
| 420 | |
| 421 | #[test] |
| 422 | fn test_tab_stops_affect_line_width() -> Result<()> { |
| 423 | let mut font_db = FontDB::new(); |
| 424 | let menlo = font_db.load_from_system("Menlo")?; |
| 425 | |
| 426 | let font_size = 13.0; |
| 427 | let tab_size = 4; |
| 428 | |
| 429 | let font_id = font_db.select_font(menlo, Properties::default()); |
| 430 | let tab_interval = (font_db |
| 431 | .space_advance_width(font_id, font_size) |
| 432 | .expect("space width should be measurable") |
| 433 | * tab_size as f64) as f32; |
| 434 | |
| 435 | let style = StyleAndFont::new(menlo, Properties::default(), TextStyle::new()); |
| 436 | let strings = "strings"; |
| 437 | let tabbed = "\t\t\tstrings"; |
| 438 | |
| 439 | let strings_line = layout_line( |
| 440 | strings, |
| 441 | LineStyle { |
| 442 | font_size, |
| 443 | line_height_ratio: 1.2, |
| 444 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 445 | fixed_width_tab_size: None, |
| 446 | }, |
| 447 | &[(0..strings.chars().count(), style)], |
| 448 | &font_db, |
| 449 | ClipConfig::default(), |
| 450 | ); |
| 451 | |
| 452 | let tabbed_line = layout_line( |
| 453 | tabbed, |
| 454 | LineStyle { |
| 455 | font_size, |
| 456 | line_height_ratio: 1.2, |
| 457 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 458 | fixed_width_tab_size: Some(tab_size), |
| 459 | }, |
| 460 | &[(0..tabbed.chars().count(), style)], |
| 461 | &font_db, |
| 462 | ClipConfig::default(), |
| 463 | ); |
| 464 | |
| 465 | // Each tab advances to the next stop. |
| 466 | let expected_width = (tab_interval * 3.0) + strings_line.width; |
| 467 | let error = (tabbed_line.width - expected_width).abs(); |
| 468 | assert!( |
| 469 | error < 1.0, |
| 470 | "expected tabbed width ~{}, got {} (error {})", |
| 471 | expected_width, |
| 472 | tabbed_line.width, |
| 473 | error |
| 474 | ); |
| 475 | |
| 476 | Ok(()) |
| 477 | } |
| 478 | |
| 479 | #[test] |
| 480 | fn test_tab_stops_do_not_drift_over_long_runs() -> Result<()> { |
| 481 | let mut font_db = FontDB::new(); |
| 482 | let menlo = font_db.load_from_system("Menlo")?; |
| 483 | |
| 484 | let font_size = 13.0; |
| 485 | let tab_size = 4; |
| 486 | |
| 487 | let font_id = font_db.select_font(menlo, Properties::default()); |
| 488 | let tab_interval = (font_db |
| 489 | .space_advance_width(font_id, font_size) |
| 490 | .expect("space width should be measurable") |
| 491 | * tab_size as f64) as f32; |
| 492 | |
| 493 | let style = StyleAndFont::new(menlo, Properties::default(), TextStyle::new()); |
| 494 | let strings = "strings"; |
| 495 | |
| 496 | let strings_line = layout_line( |
| 497 | strings, |
| 498 | LineStyle { |
| 499 | font_size, |
| 500 | line_height_ratio: 1.2, |
| 501 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 502 | fixed_width_tab_size: None, |
| 503 | }, |
| 504 | &[(0..strings.chars().count(), style)], |
| 505 | &font_db, |
| 506 | ClipConfig::default(), |
| 507 | ); |
| 508 | |
| 509 | let tab_count = 100; |
| 510 | let tabbed = format!("{}{}", "\t".repeat(tab_count), strings); |
| 511 | |
| 512 | let tabbed_line = layout_line( |
| 513 | &tabbed, |
| 514 | LineStyle { |
| 515 | font_size, |
| 516 | line_height_ratio: 1.2, |
| 517 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 518 | fixed_width_tab_size: Some(tab_size), |
| 519 | }, |
| 520 | &[(0..tabbed.chars().count(), style)], |
| 521 | &font_db, |
| 522 | ClipConfig::default(), |
| 523 | ); |
| 524 | |
| 525 | let expected_width = (tab_interval * tab_count as f32) + strings_line.width; |
| 526 | let error = (tabbed_line.width - expected_width).abs(); |
| 527 | assert!( |
| 528 | error < 1.0, |
| 529 | "expected tabbed width ~{}, got {} (error {})", |
| 530 | expected_width, |
| 531 | tabbed_line.width, |
| 532 | error |
| 533 | ); |
| 534 | |
| 535 | Ok(()) |
| 536 | } |
| 537 | |
| 538 | #[test] |
| 539 | fn test_layout_text_large_first_line_head_indent_ligatures() -> Result<()> { |
| 540 | // Similar test to above, except we have a large first line head indent which goes beyond the |
| 541 | // max_width of the first line! We expect an empty line at the start to account for this (post-layout). |
| 542 | let mut font_db = FontDB::new(); |
| 543 | let zapfino = font_db.load_from_system("Zapfino")?; |
| 544 | let menlo = font_db.load_from_system("Menlo")?; |
| 545 | |
| 546 | let text = "This is, some text, in Zapfino being laid out!"; |
| 547 | let frame = layout_text( |
| 548 | text, |
| 549 | LineStyle { |
| 550 | font_size: 16.0, |
| 551 | line_height_ratio: 1.2, |
| 552 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 553 | fixed_width_tab_size: None, |
| 554 | }, |
| 555 | &[ |
| 556 | ( |
| 557 | 0..9, |
| 558 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 559 | ), |
| 560 | ( |
| 561 | 9..22, |
| 562 | StyleAndFont::new(menlo, Properties::default(), TextStyle::new()), |
| 563 | ), |
| 564 | ( |
| 565 | 22..text.encode_utf16().count(), |
| 566 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 567 | ), |
| 568 | ], |
| 569 | &font_db, |
| 570 | 80., /* max_width */ |
| 571 | f32::MAX, /* max_height */ |
| 572 | Default::default(), |
| 573 | Some(80.), /* first_line_head_indent */ |
| 574 | ); |
| 575 | |
| 576 | // We expect 1 empty line at the start and then 7 lines of content. |
| 577 | assert_eq!(frame.lines().len(), 8); |
| 578 | |
| 579 | assert_eq!( |
| 580 | collect_glyph_indices(&frame), |
| 581 | vec![ |
| 582 | vec![], // first line head indent takes up entire line! |
| 583 | vec![0, 2, 4], |
| 584 | vec![5, 7, 8, 9, 10, 11, 12, 13], |
| 585 | vec![14, 15, 16, 17, 18, 19, 20, 21, 22], |
| 586 | vec![23, 24, 25, 27, 28], |
| 587 | vec![29, 30, 31, 32, 33, 34, 35, 36], |
| 588 | vec![37, 38, 39, 40, 41], |
| 589 | vec![42, 44, 45], |
| 590 | ] |
| 591 | ); |
| 592 | |
| 593 | Ok(()) |
| 594 | } |
| 595 | |
| 596 | #[test] |
| 597 | fn test_layout_text_last_line_clipped_ligatures() -> Result<()> { |
| 598 | let mut font_db = FontDB::new(); |
| 599 | let zapfino = font_db.load_from_system("Zapfino")?; |
| 600 | let menlo = font_db.load_from_system("Menlo")?; |
| 601 | |
| 602 | let text = "m𐍈re, Zapfino!ll𐍈, qqqq"; |
| 603 | let max_width = 180.; |
| 604 | |
| 605 | let frame = layout_text( |
| 606 | text, |
| 607 | LineStyle { |
| 608 | font_size: 16.0, |
| 609 | line_height_ratio: 1.2, |
| 610 | baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO, |
| 611 | fixed_width_tab_size: None, |
| 612 | }, |
| 613 | &[ |
| 614 | ( |
| 615 | 0..5, |
| 616 | StyleAndFont::new(menlo, Properties::default(), TextStyle::new()), |
| 617 | ), |
| 618 | ( |
| 619 | 5..13, |
| 620 | StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()), |
| 621 | ), |
| 622 | ( |
| 623 | 13..16, |
| 624 | StyleAndFont::new(menlo, Properties::default(), TextStyle::new()), |
| 625 | ), |
| 626 | ( |
| 627 | 16..text.encode_utf16().count(), |
| 628 | StyleAndFont::new(menlo, Properties::default(), TextStyle::new()), |
| 629 | ), |
| 630 | ], |
| 631 | &font_db, |
| 632 | max_width, |
| 633 | 70., /* max_height */ |
| 634 | Default::default(), |
| 635 | None, |
| 636 | ); |
| 637 | |
| 638 | // The text should only fit one line. |
| 639 | assert_eq!(frame.lines().len(), 1); |
| 640 | |
| 641 | // The text is one line long and should be clipped like so: "m𐍈re, Zapfin𐍈!l...". |
| 642 | // Note that the contents are not clipped, but the width being greater than the max width |
| 643 | // indicates that when we paint, this is clipped. |
| 644 | assert_eq!( |
| 645 | collect_glyph_indices(&frame), |
| 646 | vec![[0, 1, 2, 3, 4, 5, 6, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22],] |
| 647 | ); |
| 648 | let first_line = frame.lines().first().unwrap(); |
| 649 | assert!(first_line.width > max_width); |
| 650 | |
| 651 | Ok(()) |
| 652 | } |
| 653 |