653 lines
20 KiB
Rust
653 lines
20 KiB
Rust
use anyhow::Result;
|
|
use rand::random;
|
|
|
|
use super::*;
|
|
use crate::fonts::{
|
|
collect_glyph_indices, collect_line_caret_position_starts, init_fonts, Properties,
|
|
};
|
|
use crate::platform::FontDB as _;
|
|
use crate::text_layout::DEFAULT_TOP_BOTTOM_RATIO;
|
|
|
|
pub(crate) fn collect_line_caret_position_pairs(line: &Line) -> Vec<(usize, usize)> {
|
|
line.caret_positions
|
|
.iter()
|
|
.map(|pos| (pos.start_offset, pos.last_offset))
|
|
.collect_vec()
|
|
}
|
|
|
|
#[test]
|
|
fn test_char_indices_ligatures() -> Result<()> {
|
|
let mut font_db = FontDB::new();
|
|
let zapfino = font_db.load_from_system("Zapfino")?;
|
|
let menlo = font_db.load_from_system("Menlo")?;
|
|
|
|
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
|
let line = layout_line(
|
|
text,
|
|
LineStyle {
|
|
font_size: 16.0,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: None,
|
|
},
|
|
&[
|
|
(
|
|
0..9,
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
9..22,
|
|
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
22..text.encode_utf16().count(),
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
],
|
|
&font_db,
|
|
ClipConfig::default(),
|
|
);
|
|
|
|
// It's easiest to understand what's happening here by visualizing the text and seeing which
|
|
// characters get combined to become a single glyph. At a high level, what this is testing
|
|
// is that after laying out the string, we see some characters get combined into a single
|
|
// glyph. For example, the text "Zapfino" gets combined into a single glyph, which is why
|
|
// there is a jump from 23 to 30 in the list of glyph indices below.
|
|
// See https://docs.google.com/drawings/d/18qOKhzA5rWaMuxKVeWFDXh7ebrDjxongarAckkm0qnE/edit
|
|
// for a full diagram of what's happening here.
|
|
assert_eq!(
|
|
line.runs
|
|
.iter()
|
|
.flat_map(|r| r.glyphs.iter())
|
|
.map(|g| g.index)
|
|
.collect::<Vec<_>>(),
|
|
vec![0, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 30, 31]
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_caret_positions_ligatures() -> Result<()> {
|
|
// There's some overlap between caret positions and the character indices we
|
|
// store in glyphs. However, a single glyph may have multiple caret positions
|
|
// because characters/graphemes may get combined into a single glyph.
|
|
|
|
let mut font_db = FontDB::new();
|
|
let zapfino = font_db.load_from_system("Zapfino")?;
|
|
let menlo = font_db.load_from_system("Menlo")?;
|
|
|
|
// This string has 32 characters, but 35 UTF-16 code points and 41 UTF-8 code points.
|
|
// Each '𐍈' character encodes as 2 UTF-16 code points or 4 UTF-8 code points.
|
|
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
|
|
|
let line = layout_line(
|
|
text,
|
|
LineStyle {
|
|
font_size: 16.0,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: None,
|
|
},
|
|
&[
|
|
(
|
|
0..9,
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
9..22,
|
|
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
22..35,
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
],
|
|
&font_db,
|
|
ClipConfig::default(),
|
|
);
|
|
|
|
// There are only 23 glyphs because 'Zapfino', 'Th', and 'is' each have ligatures.
|
|
assert_eq!(
|
|
line.runs.iter().map(|run| run.glyphs.len()).sum::<usize>(),
|
|
23
|
|
);
|
|
|
|
// There should be a caret position for each character.
|
|
assert_eq!(
|
|
line.caret_positions
|
|
.iter()
|
|
.map(|pos| pos.start_offset)
|
|
.collect::<Vec<_>>(),
|
|
(0..32).collect::<Vec<usize>>()
|
|
);
|
|
|
|
// There is a caret for the 3rd character at the 3rd position, even though
|
|
// the first 2 characters are represented with 1 glyph.
|
|
assert_eq!(
|
|
line.caret_position_for_index(3),
|
|
line.caret_positions[3].position_in_line
|
|
);
|
|
|
|
// Likewise for the second 𐍈, even though it (and the previous one) take
|
|
// multiple code points.
|
|
assert_eq!(
|
|
line.caret_position_for_index(15),
|
|
line.caret_positions[15].position_in_line
|
|
);
|
|
|
|
// This tests hit-testing on a regular character.
|
|
assert_eq!(line.caret_index_for_x(0.), Some(0));
|
|
assert_eq!(line.caret_index_for_x(20.), Some(1));
|
|
|
|
// This tests hit-testing within a ligature.
|
|
assert_eq!(line.caret_index_for_x(260.), Some(25));
|
|
|
|
// This tests rounding up to the next character.
|
|
assert_eq!(line.caret_index_for_x(268.), Some(26));
|
|
|
|
// This tests a few random positions within the bound and before the last character.
|
|
let last_caret_pos = line
|
|
.caret_positions
|
|
.last()
|
|
.map_or(0., |p| p.position_in_line);
|
|
// The bounded and unbounded method should return the same result.
|
|
for _ in 0..5 {
|
|
let pos: f32 = random();
|
|
let index = line.caret_index_for_x(pos * last_caret_pos);
|
|
assert_eq!(
|
|
index,
|
|
Some(line.caret_index_for_x_unbounded(pos * last_caret_pos))
|
|
);
|
|
}
|
|
|
|
// This tests that the unbounded method returns the first index for out-of-bound position to the left
|
|
assert_eq!(line.caret_index_for_x_unbounded(-1.), line.first_index());
|
|
// The bounded method should return `None`
|
|
assert_eq!(line.caret_index_for_x(-1.), None);
|
|
|
|
// This tests that the unbounded method returns the end index for out-of-bound position to the right
|
|
assert_eq!(
|
|
line.caret_index_for_x_unbounded(line.width + 0.1),
|
|
line.end_index()
|
|
);
|
|
assert_eq!(line.caret_index_for_x(line.width + 0.1), None);
|
|
|
|
// This tests that the unbounded method returns the correct index either before or after the last glyph
|
|
assert_eq!(
|
|
line.caret_index_for_x_unbounded(0.9 * last_caret_pos + 0.1 * line.width),
|
|
line.last_index()
|
|
);
|
|
assert_eq!(
|
|
line.caret_index_for_x_unbounded(0.1 * last_caret_pos + 0.9 * line.width),
|
|
line.end_index()
|
|
);
|
|
// The bounded method should always just return the last index
|
|
assert_eq!(
|
|
line.caret_index_for_x(0.9 * last_caret_pos + 0.1 * line.width),
|
|
Some(line.last_index())
|
|
);
|
|
assert_eq!(
|
|
line.caret_index_for_x(0.1 * last_caret_pos + 0.9 * line.width),
|
|
Some(line.last_index())
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// The emojis in this test use font fallback, which means it won't behave
|
|
/// consistently across platforms.
|
|
#[test]
|
|
fn test_emoji_caret_positions() -> Result<()> {
|
|
let (font_db, font_family) = init_fonts();
|
|
|
|
// We're using these emoji specifically because they're represented as multiple
|
|
// combined characters.
|
|
let text = "👨👧👧🇨🇦";
|
|
|
|
let line = font_db.text_layout_system().layout_line(
|
|
text,
|
|
LineStyle {
|
|
font_size: 16.0,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: None,
|
|
},
|
|
&[(
|
|
0..12,
|
|
StyleAndFont::new(font_family, Properties::default(), TextStyle::new()),
|
|
)],
|
|
10000.0,
|
|
ClipConfig::default(),
|
|
);
|
|
|
|
// We want the leading edge for caret positions, so the first one is at the
|
|
// start of the line.
|
|
assert_eq!(line.caret_positions[0].position_in_line, 0.0);
|
|
|
|
assert_eq!(
|
|
collect_line_caret_position_starts(&line),
|
|
// CoreText gives us one caret position per visible character.
|
|
// Each emoji is multiple characters, but one grapheme and therefore one
|
|
// caret position.
|
|
vec![0, 5]
|
|
);
|
|
|
|
// The first character is within the first emoji, so its caret position is
|
|
// at the start of the line.
|
|
assert_eq!(line.caret_position_for_index(0), 0.0);
|
|
// Likewise, the start of the next emoji returns its start position.
|
|
assert_eq!(
|
|
line.caret_position_for_index(5),
|
|
line.caret_positions[1].position_in_line
|
|
);
|
|
// Subsequent positions within the emoji also resolve to its starting offset.
|
|
assert_eq!(
|
|
line.caret_position_for_index(6),
|
|
line.caret_positions[1].position_in_line
|
|
);
|
|
// Past the end of the last emoji, we clamp to the end of the line.
|
|
assert_eq!(line.caret_position_for_index(7), line.width);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// The RTL text and emoji in this test use font fallback, which means
|
|
/// this test won't behave consistently across platforms.
|
|
#[test]
|
|
fn test_bidi_caret_positions() -> Result<()> {
|
|
let (font_db, font_family) = init_fonts();
|
|
|
|
let text = "a שָׁלוֹם 🇨🇦 test";
|
|
let line = font_db.text_layout_system().layout_line(
|
|
text,
|
|
LineStyle {
|
|
font_size: 16.0,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: None,
|
|
},
|
|
&[(
|
|
0..text.encode_utf16().count(),
|
|
StyleAndFont::new(font_family, Properties::default(), TextStyle::new()),
|
|
)],
|
|
10000.0,
|
|
ClipConfig::default(),
|
|
);
|
|
|
|
// Caret positions should account for diacritics in the Hebrew text, as well
|
|
// as the 🇨🇦 emoji consisting of multiple characters. In addition, they should
|
|
// be sorted by display order.
|
|
assert_eq!(
|
|
collect_line_caret_position_pairs(&line),
|
|
vec![
|
|
// "a "
|
|
(0, 0),
|
|
(1, 1),
|
|
// שָׁלוֹם
|
|
(8, 8),
|
|
(6, 7),
|
|
(5, 5),
|
|
(2, 4),
|
|
// " "
|
|
(9, 9),
|
|
// 🇨🇦
|
|
(10, 11),
|
|
// " test"
|
|
(12, 12),
|
|
(13, 13),
|
|
(14, 14),
|
|
(15, 15),
|
|
(16, 16)
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_layout_text_ligatures() -> Result<()> {
|
|
let mut font_db = FontDB::new();
|
|
let zapfino = font_db.load_from_system("Zapfino")?;
|
|
let menlo = font_db.load_from_system("Menlo")?;
|
|
|
|
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
|
let frame = layout_text(
|
|
text,
|
|
LineStyle {
|
|
font_size: 16.0,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: None,
|
|
},
|
|
&[
|
|
(
|
|
0..9,
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
9..22,
|
|
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
22..text.encode_utf16().count(),
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
],
|
|
&font_db,
|
|
125., /* max_width */
|
|
f32::MAX, /* max_height */
|
|
Default::default(),
|
|
None,
|
|
);
|
|
|
|
// The text should contain multiple lines since it can't fit in 125 pixels on the first
|
|
// line.
|
|
assert_eq!(frame.lines().len(), 4);
|
|
|
|
// The text should be wrapped over 4 lines and look like this:
|
|
// "This is
|
|
// m𐍈re or
|
|
// less,
|
|
// Zapfino!𐍈"
|
|
assert_eq!(
|
|
collect_glyph_indices(&frame),
|
|
vec![
|
|
vec![0, 2, 4, 5, 7, 8],
|
|
vec![9, 10, 11, 12, 13, 14, 15, 16],
|
|
vec![17, 18, 19, 20, 21, 22,],
|
|
vec![23, 30, 31]
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_layout_text_first_line_head_indent_ligatures() -> Result<()> {
|
|
// Similar test to above, except we add in a left head indent (with reduced max width)!
|
|
let mut font_db = FontDB::new();
|
|
let zapfino = font_db.load_from_system("Zapfino")?;
|
|
let menlo = font_db.load_from_system("Menlo")?;
|
|
|
|
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
|
let frame = layout_text(
|
|
text,
|
|
LineStyle {
|
|
font_size: 16.0,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: None,
|
|
},
|
|
&[
|
|
(
|
|
0..9,
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
9..22,
|
|
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
22..text.encode_utf16().count(),
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
],
|
|
&font_db,
|
|
80., /* max_width */
|
|
f32::MAX, /* max_height */
|
|
Default::default(),
|
|
Some(50.), /* first_line_head_indent */
|
|
);
|
|
|
|
// The text should contain multiple lines since we have a 50px left head indent on the first
|
|
// line and then each line only has 80px.
|
|
assert_eq!(frame.lines().len(), 6);
|
|
|
|
assert_eq!(
|
|
collect_glyph_indices(&frame),
|
|
vec![
|
|
vec![0], // left head indent means we don't have much content laid out on this line.
|
|
vec![1, 2, 4, 5, 7, 8],
|
|
vec![9, 10, 11, 12, 13, 14, 15, 16],
|
|
vec![17, 18, 19, 20, 21, 22],
|
|
vec![23, 24, 25, 27, 28],
|
|
vec![29, 30, 31],
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_tab_stops_affect_line_width() -> Result<()> {
|
|
let mut font_db = FontDB::new();
|
|
let menlo = font_db.load_from_system("Menlo")?;
|
|
|
|
let font_size = 13.0;
|
|
let tab_size = 4;
|
|
|
|
let font_id = font_db.select_font(menlo, Properties::default());
|
|
let tab_interval = (font_db
|
|
.space_advance_width(font_id, font_size)
|
|
.expect("space width should be measurable")
|
|
* tab_size as f64) as f32;
|
|
|
|
let style = StyleAndFont::new(menlo, Properties::default(), TextStyle::new());
|
|
let strings = "strings";
|
|
let tabbed = "\t\t\tstrings";
|
|
|
|
let strings_line = layout_line(
|
|
strings,
|
|
LineStyle {
|
|
font_size,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: None,
|
|
},
|
|
&[(0..strings.chars().count(), style)],
|
|
&font_db,
|
|
ClipConfig::default(),
|
|
);
|
|
|
|
let tabbed_line = layout_line(
|
|
tabbed,
|
|
LineStyle {
|
|
font_size,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: Some(tab_size),
|
|
},
|
|
&[(0..tabbed.chars().count(), style)],
|
|
&font_db,
|
|
ClipConfig::default(),
|
|
);
|
|
|
|
// Each tab advances to the next stop.
|
|
let expected_width = (tab_interval * 3.0) + strings_line.width;
|
|
let error = (tabbed_line.width - expected_width).abs();
|
|
assert!(
|
|
error < 1.0,
|
|
"expected tabbed width ~{}, got {} (error {})",
|
|
expected_width,
|
|
tabbed_line.width,
|
|
error
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_tab_stops_do_not_drift_over_long_runs() -> Result<()> {
|
|
let mut font_db = FontDB::new();
|
|
let menlo = font_db.load_from_system("Menlo")?;
|
|
|
|
let font_size = 13.0;
|
|
let tab_size = 4;
|
|
|
|
let font_id = font_db.select_font(menlo, Properties::default());
|
|
let tab_interval = (font_db
|
|
.space_advance_width(font_id, font_size)
|
|
.expect("space width should be measurable")
|
|
* tab_size as f64) as f32;
|
|
|
|
let style = StyleAndFont::new(menlo, Properties::default(), TextStyle::new());
|
|
let strings = "strings";
|
|
|
|
let strings_line = layout_line(
|
|
strings,
|
|
LineStyle {
|
|
font_size,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: None,
|
|
},
|
|
&[(0..strings.chars().count(), style)],
|
|
&font_db,
|
|
ClipConfig::default(),
|
|
);
|
|
|
|
let tab_count = 100;
|
|
let tabbed = format!("{}{}", "\t".repeat(tab_count), strings);
|
|
|
|
let tabbed_line = layout_line(
|
|
&tabbed,
|
|
LineStyle {
|
|
font_size,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: Some(tab_size),
|
|
},
|
|
&[(0..tabbed.chars().count(), style)],
|
|
&font_db,
|
|
ClipConfig::default(),
|
|
);
|
|
|
|
let expected_width = (tab_interval * tab_count as f32) + strings_line.width;
|
|
let error = (tabbed_line.width - expected_width).abs();
|
|
assert!(
|
|
error < 1.0,
|
|
"expected tabbed width ~{}, got {} (error {})",
|
|
expected_width,
|
|
tabbed_line.width,
|
|
error
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_layout_text_large_first_line_head_indent_ligatures() -> Result<()> {
|
|
// Similar test to above, except we have a large first line head indent which goes beyond the
|
|
// max_width of the first line! We expect an empty line at the start to account for this (post-layout).
|
|
let mut font_db = FontDB::new();
|
|
let zapfino = font_db.load_from_system("Zapfino")?;
|
|
let menlo = font_db.load_from_system("Menlo")?;
|
|
|
|
let text = "This is, some text, in Zapfino being laid out!";
|
|
let frame = layout_text(
|
|
text,
|
|
LineStyle {
|
|
font_size: 16.0,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: None,
|
|
},
|
|
&[
|
|
(
|
|
0..9,
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
9..22,
|
|
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
22..text.encode_utf16().count(),
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
],
|
|
&font_db,
|
|
80., /* max_width */
|
|
f32::MAX, /* max_height */
|
|
Default::default(),
|
|
Some(80.), /* first_line_head_indent */
|
|
);
|
|
|
|
// We expect 1 empty line at the start and then 7 lines of content.
|
|
assert_eq!(frame.lines().len(), 8);
|
|
|
|
assert_eq!(
|
|
collect_glyph_indices(&frame),
|
|
vec![
|
|
vec![], // first line head indent takes up entire line!
|
|
vec![0, 2, 4],
|
|
vec![5, 7, 8, 9, 10, 11, 12, 13],
|
|
vec![14, 15, 16, 17, 18, 19, 20, 21, 22],
|
|
vec![23, 24, 25, 27, 28],
|
|
vec![29, 30, 31, 32, 33, 34, 35, 36],
|
|
vec![37, 38, 39, 40, 41],
|
|
vec![42, 44, 45],
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_layout_text_last_line_clipped_ligatures() -> Result<()> {
|
|
let mut font_db = FontDB::new();
|
|
let zapfino = font_db.load_from_system("Zapfino")?;
|
|
let menlo = font_db.load_from_system("Menlo")?;
|
|
|
|
let text = "m𐍈re, Zapfino!ll𐍈, qqqq";
|
|
let max_width = 180.;
|
|
|
|
let frame = layout_text(
|
|
text,
|
|
LineStyle {
|
|
font_size: 16.0,
|
|
line_height_ratio: 1.2,
|
|
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
|
fixed_width_tab_size: None,
|
|
},
|
|
&[
|
|
(
|
|
0..5,
|
|
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
5..13,
|
|
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
13..16,
|
|
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
|
),
|
|
(
|
|
16..text.encode_utf16().count(),
|
|
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
|
),
|
|
],
|
|
&font_db,
|
|
max_width,
|
|
70., /* max_height */
|
|
Default::default(),
|
|
None,
|
|
);
|
|
|
|
// The text should only fit one line.
|
|
assert_eq!(frame.lines().len(), 1);
|
|
|
|
// The text is one line long and should be clipped like so: "m𐍈re, Zapfin𐍈!l...".
|
|
// Note that the contents are not clipped, but the width being greater than the max width
|
|
// indicates that when we paint, this is clipped.
|
|
assert_eq!(
|
|
collect_glyph_indices(&frame),
|
|
vec![[0, 1, 2, 3, 4, 5, 6, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22],]
|
|
);
|
|
let first_line = frame.lines().first().unwrap();
|
|
assert!(first_line.width > max_width);
|
|
|
|
Ok(())
|
|
}
|