mod helpers;
use helpers::{
NOTO_SANS, Rect, RenderFrameExt, Typesetter, assert_caret_is_real, assert_no_glyph_overlap,
make_block_at, make_cell_at, make_frame, make_table,
};
use text_typeset::layout::table::{CellLayoutParams, TableLayoutParams};
use text_typeset::{CursorDisplay, DecorationKind};
fn make_3x2_table() -> TableLayoutParams {
make_table(
1,
3,
2,
vec![
make_cell_at(0, 0, 10, 0, "Header A"),
make_cell_at(0, 1, 11, 9, "Header B"),
make_cell_at(1, 0, 12, 18, "Cell one"),
make_cell_at(1, 1, 13, 27, "Cell two"),
make_cell_at(2, 0, 14, 36, "Cell three"),
make_cell_at(2, 1, 15, 47, "Cell four"),
],
)
}
fn setup_table(viewport_width: f32) -> Typesetter {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(viewport_width, 600.0);
let table = make_3x2_table();
ts.add_table(&table);
ts.render();
ts
}
#[test]
fn table_has_positive_dimensions() {
let ts = setup_table(800.0);
let height = ts.content_height();
assert!(
height > 0.0,
"table content height should be positive: {}",
height
);
}
#[test]
fn table_rows_have_increasing_y() {
let ts = setup_table(800.0);
let y0 = ts.caret_rect(0)[1]; let y1 = ts.caret_rect(18)[1]; let y2 = ts.caret_rect(36)[1];
assert!(y1 > y0, "row 1 y ({}) should be below row 0 y ({})", y1, y0);
assert!(y2 > y1, "row 2 y ({}) should be below row 1 y ({})", y2, y1);
}
#[test]
fn table_columns_have_increasing_x() {
let ts = setup_table(800.0);
let x_col0 = ts.caret_rect(0)[0]; let x_col1 = ts.caret_rect(9)[0];
assert!(
x_col1 > x_col0,
"col 1 x ({}) should be right of col 0 x ({})",
x_col1,
x_col0
);
}
#[test]
fn table_same_row_cells_share_y() {
let ts = setup_table(800.0);
let y_col0 = ts.caret_rect(0)[1];
let y_col1 = ts.caret_rect(9)[1];
assert!(
(y_col0 - y_col1).abs() < 1.0,
"same-row cells should have same y: col0={}, col1={}",
y_col0,
y_col1
);
}
#[test]
fn table_column_widths_finite_at_infinite_viewport() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(f32::INFINITY, 600.0);
ts.set_content_width(f32::INFINITY);
let table = make_3x2_table();
ts.add_table(&table);
ts.render();
for pos in [0, 9, 18, 27, 36, 47] {
let rect = ts.caret_rect(pos);
assert!(
rect[0].is_finite(),
"caret x should be finite at pos {}: {}",
pos,
rect[0]
);
}
}
#[test]
fn table_column_widths_positive_at_narrow_viewport() {
let ts = setup_table(100.0);
for pos in [0, 9, 18, 27, 36, 47] {
let rect = ts.caret_rect(pos);
assert!(rect[0].is_finite(), "caret x finite at pos {}", pos);
assert!(rect[3] > 0.0, "caret h positive at pos {}", pos);
}
}
#[test]
fn caret_rect_found_for_all_cell_positions() {
let ts = setup_table(800.0);
for pos in [
0, 4, 8, 9, 13, 17, 18, 22, 26, 27, 31, 35, 36, 40, 45, 47, 51, 57,
] {
assert_caret_is_real(ts.caret_rect(pos), &format!("pos {}", pos));
}
}
#[test]
fn caret_y_monotonic_across_cells() {
let ts = setup_table(800.0);
let positions = [0, 9, 18, 27, 36, 47]; let mut prev_y = -1.0f32;
for &pos in &positions {
let rect = ts.caret_rect(pos);
let y = rect[1];
assert!(
y >= prev_y - 1.0,
"caret y should not decrease: prev={}, pos {} y={}",
prev_y,
pos,
y
);
prev_y = y;
}
}
#[test]
fn caret_x_advances_within_cell() {
let ts = setup_table(800.0);
let x_start = ts.caret_rect(0)[0];
let x_mid = ts.caret_rect(4)[0];
let x_end = ts.caret_rect(8)[0];
assert!(
x_mid > x_start,
"x should advance: start={}, mid={}",
x_start,
x_mid
);
assert!(
x_end > x_mid,
"x should advance: mid={}, end={}",
x_mid,
x_end
);
}
#[test]
fn hit_test_finds_cells_in_all_rows() {
let ts = setup_table(800.0);
for (label, pos) in [("row 0", 0), ("row 1", 18), ("row 2", 36)] {
let rect = ts.caret_rect(pos);
let hx = rect[0].max(1.0);
let hy = rect[1] + rect[3] * 0.5;
let hit = ts.hit_test(hx, hy);
assert!(
hit.is_some(),
"hit_test should find {} at ({}, {})",
label,
hx,
hy
);
}
}
#[test]
fn hit_test_returns_table_cell_blocks() {
let ts = setup_table(800.0);
let rect = ts.caret_rect(18);
let hit = ts
.hit_test(rect[0].max(1.0), rect[1] + rect[3] * 0.5)
.unwrap();
assert_eq!(hit.block_id, 12, "hit should find block 12 (Cell one)");
assert!(
ts.block_visual_info(hit.block_id).is_none(),
"table cell block should not have block_visual_info"
);
assert!(
ts.is_block_in_table(hit.block_id),
"table cell block should be in table"
);
}
#[test]
fn table_renders_glyphs_for_all_cells() {
let mut ts = setup_table(800.0);
let frame = ts.render();
assert!(
frame.glyphs.len() > 20,
"table should render many glyphs, got {}",
frame.glyphs.len()
);
}
#[test]
fn table_renders_border_decorations() {
let mut ts = setup_table(800.0);
let frame = ts.render();
let borders: Vec<_> = frame
.decorations
.iter()
.filter(|d| d.kind == text_typeset::DecorationKind::BlockBackground)
.collect();
assert!(
!borders.is_empty() || !frame.decorations.is_empty(),
"table should produce decorations"
);
}
#[test]
fn table_cell_glyphs_within_cell_bounds() {
let mut ts = setup_table(800.0);
let frame = ts.render();
for (i, glyph) in frame.glyphs.iter().enumerate() {
let r = Rect::from(glyph.screen);
assert!(
r.x().is_finite() && r.y().is_finite(),
"glyph[{}] has non-finite position: {}",
i,
r
);
assert!(r.x() >= 0.0, "glyph[{}] has negative x: {}", i, r);
}
}
#[test]
fn vertical_hit_test_reaches_all_rows() {
let ts = setup_table(800.0);
let r0 = Rect::from(ts.caret_rect(0)); let r1 = Rect::from(ts.caret_rect(18)); let r2 = Rect::from(ts.caret_rect(36));
let x = r0.x().max(1.0);
let hit1 = ts.hit_test(x, r1.y() + r1.h() * 0.5);
assert!(hit1.is_some(), "should hit row 1");
let hit1 = hit1.unwrap();
assert_eq!(hit1.block_id, 12, "should hit row 1 col 0 block (12)");
let hit2 = ts.hit_test(x, r2.y() + r2.h() * 0.5);
assert!(hit2.is_some(), "should hit row 2");
let hit2 = hit2.unwrap();
assert_eq!(hit2.block_id, 14, "should hit row 2 col 0 block (14)");
}
#[test]
fn hit_test_between_rows_finds_nearest() {
let ts = setup_table(800.0);
let r0 = Rect::from(ts.caret_rect(0)); let r1 = Rect::from(ts.caret_rect(18));
let mid_y = (r0.bottom() + r1.y()) / 2.0;
let hit = ts.hit_test(r0.x().max(1.0), mid_y);
assert!(
hit.is_some(),
"hit_test should find something between rows at y={}",
mid_y
);
}
#[test]
fn relayout_table_block_updates_cell_content() {
let mut ts = setup_table(800.0);
let before_height = ts.content_height();
let glyph_count_before = ts.render().glyphs.len();
let updated = make_block_at(12, 18, "Cell one now has much more text to wrap around");
ts.relayout_block(&updated);
ts.render();
let after_height = ts.content_height();
let glyph_count_after = ts.render().glyphs.len();
assert!(
after_height >= before_height,
"table should grow: before={}, after={}",
before_height,
after_height
);
assert!(
glyph_count_after > glyph_count_before,
"more glyphs after longer text: before={}, after={}",
glyph_count_before,
glyph_count_after
);
}
#[test]
fn render_block_only_preserves_table_cell_glyphs() {
let mut ts = setup_table(800.0);
let full_glyph_count = ts.render().glyphs.len();
assert!(full_glyph_count > 0, "table should render glyphs");
let updated = make_block_at(12, 18, "Updated!");
ts.relayout_block(&updated);
let frame = ts.render_block_only(12);
assert!(
frame.glyphs.len() > 10,
"table cell glyphs should be preserved after render_block_only, got {}",
frame.glyphs.len()
);
}
#[test]
fn hit_test_in_inter_row_gap_snaps_to_nearest_row() {
let ts = setup_table(800.0);
let r0 = Rect::from(ts.caret_rect(0)); let r1 = Rect::from(ts.caret_rect(18));
let gap_y = (r0.bottom() + r1.y()) / 2.0;
let x = r0.x().max(1.0);
let hit = ts.hit_test(x, gap_y);
assert!(
hit.is_some(),
"hit_test in inter-row gap at y={} should snap to a row",
gap_y
);
let bid = hit.unwrap().block_id;
assert!(
bid == 10 || bid == 12,
"should hit row 0 (block 10) or row 1 (block 12), got block {}",
bid
);
}
#[test]
fn hit_test_just_above_row_content_finds_row() {
let ts = setup_table(800.0);
let r1 = Rect::from(ts.caret_rect(18));
let hit = ts.hit_test(r1.x().max(1.0), r1.y() - 1.0);
assert!(
hit.is_some(),
"hit_test just above row 1 content should still find a cell"
);
}
#[test]
fn table_custom_column_widths_respected() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
let table = TableLayoutParams {
table_id: 1,
rows: 1,
columns: 2,
column_widths: vec![1.0, 3.0],
border_width: 1.0,
cell_spacing: 0.0,
cell_padding: 4.0,
cells: vec![
make_cell_at(0, 0, 10, 0, "Narrow"),
make_cell_at(0, 1, 11, 7, "Wide column"),
],
};
ts.add_table(&table);
ts.render();
let x0 = ts.caret_rect(0)[0];
let x1 = ts.caret_rect(7)[0];
assert!(
x1 > x0,
"col 1 x ({}) should be right of col 0 x ({})",
x1,
x0
);
let mut ts_even = Typesetter::new();
let face_e = ts_even.register_font(NOTO_SANS);
ts_even.set_default_font(face_e, 16.0);
ts_even.set_viewport(800.0, 600.0);
let table_even = TableLayoutParams {
table_id: 1,
rows: 1,
columns: 2,
column_widths: vec![], border_width: 1.0,
cell_spacing: 0.0,
cell_padding: 4.0,
cells: vec![
make_cell_at(0, 0, 10, 0, "Narrow"),
make_cell_at(0, 1, 11, 7, "Wide column"),
],
};
ts_even.add_table(&table_even);
ts_even.render();
let x1_even = ts_even.caret_rect(7)[0];
assert!(
x1 < x1_even,
"with 1:3 widths, col 1 should start earlier ({}) than even ({})",
x1,
x1_even
);
}
#[test]
fn table_cell_spacing_creates_gaps_between_rows() {
let mut ts0 = Typesetter::new();
let f0 = ts0.register_font(NOTO_SANS);
ts0.set_default_font(f0, 16.0);
ts0.set_viewport(800.0, 600.0);
let table0 = TableLayoutParams {
table_id: 1,
rows: 2,
columns: 1,
column_widths: vec![],
border_width: 1.0,
cell_spacing: 0.0,
cell_padding: 4.0,
cells: vec![
make_cell_at(0, 0, 10, 0, "Row A"),
make_cell_at(1, 0, 11, 6, "Row B"),
],
};
ts0.add_table(&table0);
ts0.render();
let h0 = ts0.content_height();
let y0_r0 = ts0.caret_rect(0)[1];
let y0_r1 = ts0.caret_rect(6)[1];
let mut ts1 = Typesetter::new();
let f1 = ts1.register_font(NOTO_SANS);
ts1.set_default_font(f1, 16.0);
ts1.set_viewport(800.0, 600.0);
let table1 = TableLayoutParams {
table_id: 1,
rows: 2,
columns: 1,
column_widths: vec![],
border_width: 1.0,
cell_spacing: 10.0,
cell_padding: 4.0,
cells: vec![
make_cell_at(0, 0, 10, 0, "Row A"),
make_cell_at(1, 0, 11, 6, "Row B"),
],
};
ts1.add_table(&table1);
ts1.render();
let h1 = ts1.content_height();
let y1_r0 = ts1.caret_rect(0)[1];
let y1_r1 = ts1.caret_rect(6)[1];
assert!(
h1 > h0,
"spaced table should be taller: no_spacing={}, spacing={}",
h0,
h1
);
let gap_without = y0_r1 - y0_r0;
let gap_with = y1_r1 - y1_r0;
assert!(
gap_with > gap_without,
"row gap with spacing ({}) should exceed without ({})",
gap_with,
gap_without
);
}
#[test]
fn table_single_row_layout() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
let table = make_table(
1,
1,
2,
vec![
make_cell_at(0, 0, 10, 0, "Left"),
make_cell_at(0, 1, 11, 5, "Right"),
],
);
ts.add_table(&table);
ts.render();
assert!(ts.content_height() > 0.0);
assert_caret_is_real(ts.caret_rect(0), "single row col 0");
assert_caret_is_real(ts.caret_rect(5), "single row col 1");
let frame = ts.render();
let border_count = frame.decoration_count(DecorationKind::TableBorder);
assert_eq!(
border_count, 5,
"1-row 2-col table should have 5 border decorations, got {}",
border_count
);
}
#[test]
fn table_single_column_layout() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
let table = make_table(
1,
2,
1,
vec![
make_cell_at(0, 0, 10, 0, "Top"),
make_cell_at(1, 0, 11, 4, "Bottom"),
],
);
ts.add_table(&table);
ts.render();
let y0 = ts.caret_rect(0)[1];
let y1 = ts.caret_rect(4)[1];
assert!(y1 > y0, "row 1 y ({}) should be below row 0 y ({})", y1, y0);
let frame = ts.render();
let border_count = frame.decoration_count(DecorationKind::TableBorder);
assert_eq!(
border_count, 5,
"2-row 1-col table should have 5 border decorations, got {}",
border_count
);
}
#[test]
fn table_multiple_blocks_per_cell() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
let table = TableLayoutParams {
table_id: 1,
rows: 1,
columns: 1,
column_widths: vec![],
border_width: 1.0,
cell_spacing: 0.0,
cell_padding: 4.0,
cells: vec![CellLayoutParams {
row: 0,
column: 0,
blocks: vec![
make_block_at(20, 0, "First paragraph"),
make_block_at(21, 16, "Second paragraph"),
],
background_color: None,
}],
};
ts.add_table(&table);
ts.render();
assert_caret_is_real(ts.caret_rect(0), "first block start");
assert_caret_is_real(ts.caret_rect(16), "second block start");
let y0 = ts.caret_rect(0)[1];
let y1 = ts.caret_rect(16)[1];
assert!(
y1 > y0,
"second block y ({}) should be below first ({})",
y1,
y0
);
}
#[test]
fn table_out_of_bounds_cell_silently_skipped() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
let table = TableLayoutParams {
table_id: 1,
rows: 1,
columns: 1,
column_widths: vec![],
border_width: 1.0,
cell_spacing: 0.0,
cell_padding: 4.0,
cells: vec![
make_cell_at(0, 0, 10, 0, "Valid"),
make_cell_at(5, 5, 99, 100, "Out of bounds"),
],
};
ts.add_table(&table);
ts.render();
assert!(
ts.content_height() > 0.0,
"table should have positive height"
);
assert_caret_is_real(ts.caret_rect(0), "valid cell");
let frame = ts.render();
assert!(!frame.glyphs.is_empty(), "valid cell should produce glyphs");
}
#[test]
fn single_char_relayout_does_not_grow_row_height() {
let mut ts = setup_table(800.0);
ts.render();
let height_before = ts.content_height();
let updated = make_block_at(12, 18, "Cell one!");
ts.relayout_block(&updated);
ts.render();
let height_after = ts.content_height();
assert!(
(height_after - height_before).abs() < 0.01,
"adding one char should not grow the table: before={}, after={}",
height_before,
height_after
);
}
#[test]
fn table_zero_border_produces_no_border_decorations() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
let table = TableLayoutParams {
table_id: 1,
rows: 2,
columns: 2,
column_widths: vec![],
border_width: 0.0,
cell_spacing: 0.0,
cell_padding: 4.0,
cells: vec![
make_cell_at(0, 0, 10, 0, "A"),
make_cell_at(0, 1, 11, 2, "B"),
make_cell_at(1, 0, 12, 4, "C"),
make_cell_at(1, 1, 13, 6, "D"),
],
};
ts.add_table(&table);
let frame = ts.render();
let border_count = frame.decoration_count(DecorationKind::TableBorder);
assert_eq!(
border_count, 0,
"zero border_width should produce no TableBorder decorations, got {}",
border_count
);
assert!(!frame.glyphs.is_empty(), "glyphs should still render");
}
#[test]
fn table_cell_background_produces_decoration() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
let table = TableLayoutParams {
table_id: 1,
rows: 2,
columns: 2,
column_widths: vec![],
border_width: 1.0,
cell_spacing: 0.0,
cell_padding: 4.0,
cells: vec![
CellLayoutParams {
row: 0,
column: 0,
blocks: vec![make_block_at(10, 0, "Highlighted")],
background_color: Some([1.0, 0.0, 0.0, 0.5]),
},
make_cell_at(0, 1, 11, 12, "Normal"),
make_cell_at(1, 0, 12, 19, "Normal"),
make_cell_at(1, 1, 13, 26, "Normal"),
],
};
ts.add_table(&table);
let frame = ts.render();
let bg_rects = frame.decorations_of(DecorationKind::TableCellBackground);
assert_eq!(
bg_rects.len(),
1,
"exactly one cell has a background, got {}",
bg_rects.len()
);
let bg = &bg_rects[0];
assert!(bg.w() > 0.0, "background should have positive width");
assert!(bg.h() > 0.0, "background should have positive height");
}
#[test]
fn table_border_decoration_count_matches_structure() {
let mut ts = setup_table(800.0);
let frame = ts.render();
let border_count = frame.decoration_count(DecorationKind::TableBorder);
assert_eq!(
border_count, 7,
"3x2 table should have 7 border decorations, got {}",
border_count
);
}
#[test]
fn table_no_glyph_overlap_across_cells() {
let mut ts = setup_table(800.0);
let frame = ts.render();
assert_no_glyph_overlap(frame);
}
#[test]
fn hit_test_at_column_boundary_snaps_to_nearest_column() {
let ts = setup_table(800.0);
let x_col0_end = ts.caret_rect(8)[0]; let x_col1_start = ts.caret_rect(9)[0]; let mid_x = (x_col0_end + x_col1_start) / 2.0;
let row_y = ts.caret_rect(0)[1] + ts.caret_rect(0)[3] * 0.5;
let hit = ts.hit_test(mid_x, row_y);
assert!(
hit.is_some(),
"hit_test at column boundary x={} should find a cell",
mid_x
);
let bid = hit.unwrap().block_id;
assert!(
bid == 10 || bid == 11,
"should snap to col 0 (block 10) or col 1 (block 11), got block {}",
bid
);
}
#[test]
fn hit_test_far_right_of_table_snaps_to_last_column() {
let ts = setup_table(800.0);
let row_y = ts.caret_rect(0)[1] + ts.caret_rect(0)[3] * 0.5;
let hit = ts.hit_test(2000.0, row_y);
assert!(
hit.is_some(),
"hit_test far right should snap to last column"
);
assert_eq!(
hit.unwrap().block_id,
11,
"far right should snap to last column block"
);
}
#[test]
fn hit_test_above_table_top_finds_first_row() {
let ts = setup_table(800.0);
let r0 = Rect::from(ts.caret_rect(0));
let hit = ts.hit_test(r0.x().max(1.0), r0.y() - 5.0);
assert!(
hit.is_some(),
"hit_test above table top should still find a cell"
);
}
#[test]
fn is_block_in_table_false_for_non_table_block() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
ts.layout_blocks(vec![make_block_at(1, 0, "Standalone")]);
let table = make_table(1, 1, 1, vec![make_cell_at(0, 0, 20, 12, "In table")]);
ts.add_table(&table);
ts.render();
assert!(
!ts.is_block_in_table(1),
"standalone block should not be in table"
);
assert!(
ts.is_block_in_table(20),
"table cell block should be in table"
);
}
#[test]
fn selection_within_single_table_cell() {
let mut ts = setup_table(800.0);
ts.set_cursor(&CursorDisplay {
position: 4,
anchor: 0,
visible: true,
selected_cells: vec![],
});
let frame = ts.render();
let sel = frame.selection_rects();
assert!(
!sel.is_empty(),
"selection inside a table cell should produce selection rects"
);
for (i, r) in sel.iter().enumerate() {
assert!(
r.w() > 0.0,
"selection rect[{}] should have positive width",
i
);
assert!(
r.h() > 0.0,
"selection rect[{}] should have positive height",
i
);
}
}
#[test]
fn selection_across_table_cells() {
let mut ts = setup_table(800.0);
ts.set_cursor(&CursorDisplay {
position: 22,
anchor: 0,
visible: true,
selected_cells: vec![],
});
let frame = ts.render();
let sel = frame.selection_rects();
assert!(
sel.len() >= 2,
"cross-cell selection should produce multiple rects, got {}",
sel.len()
);
let min_y = sel.iter().map(|r| r.y()).fold(f32::MAX, f32::min);
let max_y = sel.iter().map(|r| r.bottom()).fold(f32::MIN, f32::max);
assert!(
max_y - min_y > 10.0,
"selection should span multiple rows: min_y={}, max_y={}",
min_y,
max_y
);
}
#[test]
fn cursor_visible_in_table_cell() {
let mut ts = setup_table(800.0);
ts.set_cursor(&CursorDisplay {
position: 18,
anchor: 18,
visible: true,
selected_cells: vec![],
});
let frame = ts.render();
let cursor = frame.cursor_rect();
assert!(cursor.is_some(), "cursor should be visible in table cell");
let c = cursor.unwrap();
assert!(c.h() > 0.0, "cursor should have positive height");
let row1_y = ts.caret_rect(18)[1];
assert!(
(c.y() - row1_y).abs() < 5.0,
"cursor y ({}) should be near row 1 caret y ({})",
c.y(),
row1_y
);
}
#[test]
fn two_tables_in_sequence() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
let table1 = make_table(1, 1, 1, vec![make_cell_at(0, 0, 10, 0, "Table one")]);
ts.add_table(&table1);
let height_after_one = ts.content_height();
let table2 = make_table(2, 1, 1, vec![make_cell_at(0, 0, 20, 10, "Table two")]);
ts.add_table(&table2);
ts.render();
let height_after_two = ts.content_height();
assert!(
height_after_two > height_after_one,
"two tables should be taller than one: one={}, two={}",
height_after_one,
height_after_two
);
let y1 = ts.caret_rect(0)[1];
let y2 = ts.caret_rect(10)[1];
assert!(
y2 > y1,
"table 2 caret y ({}) should be below table 1 ({})",
y2,
y1
);
}
#[test]
fn scroll_offset_shifts_table_caret_positions() {
let mut ts = setup_table(800.0);
ts.render();
let y_before = ts.caret_rect(0)[1];
ts.set_scroll_offset(50.0);
ts.render();
let y_after = ts.caret_rect(0)[1];
let delta = y_before - y_after;
assert!(
(delta - 50.0).abs() < 1.0,
"scroll offset of 50 should shift caret y by ~50: before={}, after={}, delta={}",
y_before,
y_after,
delta
);
}
#[test]
fn content_height_unchanged_after_same_text_relayout() {
let mut ts = setup_table(800.0);
ts.render();
let height_before = ts.content_height();
let same = make_block_at(12, 18, "Cell one");
ts.relayout_block(&same);
ts.render();
let height_after = ts.content_height();
assert!(
(height_after - height_before).abs() < 0.01,
"relayout with same text should not change height: before={}, after={}",
height_before,
height_after
);
}
#[test]
fn hit_test_table_cells_report_table_id() {
let ts = setup_table(800.0);
for (label, pos) in [("row 0", 0usize), ("row 1", 18), ("row 2", 36)] {
let rect = ts.caret_rect(pos);
let hx = rect[0].max(1.0);
let hy = rect[1] + rect[3] * 0.5;
let hit = ts.hit_test(hx, hy).unwrap();
assert_eq!(
hit.table_id,
Some(1),
"cell {} hit should report table_id=Some(1), got {:?}",
label,
hit.table_id
);
}
}
#[test]
fn hit_test_all_cells_have_consistent_table_id() {
let ts = setup_table(800.0);
for (pos, label) in [
(0, "row0 col0"),
(9, "row0 col1"),
(18, "row1 col0"),
(27, "row1 col1"),
(36, "row2 col0"),
(47, "row2 col1"),
] {
let rect = ts.caret_rect(pos);
let hit = ts
.hit_test(rect[0].max(1.0), rect[1] + rect[3] * 0.5)
.unwrap();
assert_eq!(
hit.table_id,
Some(1),
"cell {} (pos {}) should report table_id=Some(1), got {:?}",
label,
pos,
hit.table_id
);
}
}
#[test]
fn hit_test_block_before_table_has_no_table_id() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
ts.layout_blocks(vec![make_block_at(99, 0, "Before table")]);
ts.add_table(&make_3x2_table());
ts.render();
let info = ts.block_visual_info(99).unwrap();
let hit = ts.hit_test(40.0, info.y + info.height * 0.5).unwrap();
assert_eq!(
hit.table_id, None,
"block hit before table should have table_id=None, got {:?}",
hit.table_id
);
}
#[test]
fn hit_test_two_tables_return_correct_ids() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
let table1 = make_table(
1,
1,
2,
vec![
make_cell_at(0, 0, 10, 0, "T1-A"),
make_cell_at(0, 1, 11, 5, "T1-B"),
],
);
let table2 = make_table(
2,
1,
2,
vec![
make_cell_at(0, 0, 20, 100, "T2-A"),
make_cell_at(0, 1, 21, 105, "T2-B"),
],
);
ts.add_table(&table1);
ts.add_table(&table2);
ts.render();
let r1 = ts.caret_rect(0);
let hit1 = ts.hit_test(r1[0].max(1.0), r1[1] + r1[3] * 0.5).unwrap();
assert_eq!(
hit1.table_id,
Some(1),
"first table hit should report table_id=Some(1), got {:?}",
hit1.table_id
);
let r2 = ts.caret_rect(100);
let hit2 = ts.hit_test(r2[0].max(1.0), r2[1] + r2[3] * 0.5).unwrap();
assert_eq!(
hit2.table_id,
Some(2),
"second table hit should report table_id=Some(2), got {:?}",
hit2.table_id
);
}
#[test]
fn hit_test_table_inside_frame_has_table_id() {
let mut ts = Typesetter::new();
let face = ts.register_font(NOTO_SANS);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
let table = make_table(
7,
1,
2,
vec![
make_cell_at(0, 0, 30, 0, "Frame T-A"),
make_cell_at(0, 1, 31, 10, "Frame T-B"),
],
);
let mut frame = make_frame(5, vec![]);
frame.tables = vec![(0, table)];
ts.add_frame(&frame);
ts.render();
let rect = ts.caret_rect(0);
let hit = ts
.hit_test(rect[0].max(1.0), rect[1] + rect[3] * 0.5)
.unwrap();
assert_eq!(
hit.table_id,
Some(7),
"table inside frame should report table_id=Some(7), got {:?}",
hit.table_id
);
}