452 lines
14 KiB
Rust
452 lines
14 KiB
Rust
use pathfinder_color::ColorU;
|
|
use std::{cell::Cell, sync::Arc};
|
|
use string_offset::CharOffset;
|
|
use galaxyui::{
|
|
elements::{Axis, scroll_delta_for_pointer_movement},
|
|
fonts::FamilyId,
|
|
geometry::{rect::RectF, vector::vec2f},
|
|
text_layout::TextFrame,
|
|
units::{IntoPixels, Pixels},
|
|
};
|
|
|
|
use crate::{
|
|
content::text::{FormattedTable, table_cell_offset_maps},
|
|
render::{
|
|
element::table::{
|
|
model_table_layout_report, row_geometry_from_layout_report,
|
|
table_cursor_relative_offset, table_horizontal_scroll_delta, table_scroll_data,
|
|
table_scrollbar, table_selection_relative_range,
|
|
},
|
|
model::{
|
|
BlockSpacing, CellLayout, LaidOutTable, RenderedSelection, TableBlockConfig,
|
|
TableStyle, table_offset_map::TableOffsetMap,
|
|
},
|
|
},
|
|
};
|
|
|
|
fn test_laid_out_table() -> LaidOutTable {
|
|
let source = "h1\th2\nr1\tr2\nx\ty\n";
|
|
let table = FormattedTable::from_internal_format(source);
|
|
let cell_offset_maps = table_cell_offset_maps(&table, source);
|
|
let row_heights = vec![24.0.into_pixels(), 30.0.into_pixels(), 42.0.into_pixels()];
|
|
let column_widths = vec![80.0.into_pixels(), 120.0.into_pixels()];
|
|
let total_height = row_heights
|
|
.iter()
|
|
.fold(Pixels::zero(), |acc, row_height| acc + *row_height);
|
|
|
|
let mut row_y_offsets = Vec::with_capacity(row_heights.len() + 1);
|
|
row_y_offsets.push(0.0);
|
|
let mut running_y = 0.0;
|
|
for row_height in &row_heights {
|
|
running_y += row_height.as_f32();
|
|
row_y_offsets.push(running_y);
|
|
}
|
|
|
|
let mut col_x_offsets = Vec::with_capacity(column_widths.len() + 1);
|
|
col_x_offsets.push(0.0);
|
|
let mut running_x = 0.0;
|
|
for column_width in &column_widths {
|
|
running_x += column_width.as_f32();
|
|
col_x_offsets.push(running_x);
|
|
}
|
|
|
|
let offset_map = TableOffsetMap::new(
|
|
cell_offset_maps
|
|
.iter()
|
|
.map(|row| {
|
|
row.iter()
|
|
.map(|cell| cell.source_length().as_usize())
|
|
.collect()
|
|
})
|
|
.collect(),
|
|
);
|
|
let content_length = offset_map.total_length();
|
|
let config = TableBlockConfig {
|
|
width: 200.0.into_pixels(),
|
|
spacing: BlockSpacing::default(),
|
|
style: TableStyle {
|
|
border_color: ColorU::new(10, 11, 12, 255),
|
|
header_background: ColorU::new(20, 21, 22, 255),
|
|
cell_background: ColorU::new(30, 31, 32, 255),
|
|
alternate_row_background: None,
|
|
text_color: ColorU::new(40, 41, 42, 255),
|
|
header_text_color: ColorU::new(50, 51, 52, 255),
|
|
scrollbar_nonactive_thumb_color: ColorU::new(60, 61, 62, 255),
|
|
scrollbar_active_thumb_color: ColorU::new(70, 71, 72, 255),
|
|
font_family: FamilyId(0),
|
|
font_size: 14.0,
|
|
cell_padding: 6.0,
|
|
outer_border: true,
|
|
column_dividers: true,
|
|
row_dividers: true,
|
|
},
|
|
};
|
|
let cell_layouts = vec![vec![CellLayout::default(); 2]; 3];
|
|
let cell_text_frames = vec![vec![Arc::new(TextFrame::mock("")); 2]; 3];
|
|
|
|
LaidOutTable {
|
|
table,
|
|
config,
|
|
row_heights,
|
|
column_widths,
|
|
total_height,
|
|
offset_map,
|
|
content_length,
|
|
cell_offset_maps,
|
|
row_y_offsets,
|
|
col_x_offsets,
|
|
cell_text_frames,
|
|
cell_layouts,
|
|
cell_links: vec![
|
|
vec![vec![], vec![]],
|
|
vec![vec![], vec![]],
|
|
vec![vec![], vec![]],
|
|
],
|
|
scroll_left: Cell::new(Pixels::zero()),
|
|
scrollbar_interaction_state: Default::default(),
|
|
horizontal_scroll_allowed: true,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn model_table_layout_report_matches_model_geometry() {
|
|
let laid_out_table = test_laid_out_table();
|
|
let report = model_table_layout_report(&laid_out_table);
|
|
|
|
assert_eq!(report.column_widths, vec![80.0, 120.0]);
|
|
assert_eq!(report.column_lefts, vec![0.0, 80.0]);
|
|
assert_eq!(report.header_height, 24.0);
|
|
assert_eq!(report.row_heights, vec![30.0, 42.0]);
|
|
assert_eq!(report.row_tops, vec![0.0, 30.0]);
|
|
assert_eq!(report.total_height, 96.0);
|
|
}
|
|
|
|
#[test]
|
|
fn row_geometry_from_layout_report_includes_header_row() {
|
|
let report = model_table_layout_report(&test_laid_out_table());
|
|
let (row_tops, row_heights) = row_geometry_from_layout_report(&report);
|
|
|
|
assert_eq!(row_tops, vec![0.0, 24.0, 54.0]);
|
|
assert_eq!(row_heights, vec![24.0, 30.0, 42.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn coordinate_to_offset_returns_cell_start_for_first_cell() {
|
|
let table = test_laid_out_table();
|
|
let offset = table.coordinate_to_offset(10.0, 5.0);
|
|
assert_eq!(offset, CharOffset::zero());
|
|
}
|
|
|
|
#[test]
|
|
fn coordinate_to_offset_targets_second_column() {
|
|
let table = test_laid_out_table();
|
|
let offset = table.coordinate_to_offset(90.0, 5.0);
|
|
let cell_range = table.offset_map.cell_range(0, 1);
|
|
assert!(cell_range.is_some());
|
|
let range = cell_range.expect("cell (0,1) should exist");
|
|
assert!(
|
|
offset >= range.start && offset <= range.end,
|
|
"offset {offset:?} should be within cell (0,1) range {:?}..{:?}",
|
|
range.start,
|
|
range.end,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn coordinate_to_offset_targets_second_row() {
|
|
let table = test_laid_out_table();
|
|
let offset = table.coordinate_to_offset(10.0, 30.0);
|
|
let cell_range = table.offset_map.cell_range(1, 0);
|
|
assert!(cell_range.is_some());
|
|
let range = cell_range.expect("cell (1,0) should exist");
|
|
assert!(
|
|
offset >= range.start && offset <= range.end,
|
|
"offset {offset:?} should be within cell (1,0) range {:?}..{:?}",
|
|
range.start,
|
|
range.end,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cells_in_range_single_cell() {
|
|
let table = test_laid_out_table();
|
|
let cells = table
|
|
.offset_map
|
|
.cells_in_range(CharOffset::from(0usize), CharOffset::from(2usize));
|
|
assert_eq!(cells.len(), 1);
|
|
assert_eq!(cells[0].row, 0);
|
|
assert_eq!(cells[0].col, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn cells_in_range_across_row() {
|
|
let table = test_laid_out_table();
|
|
let cells = table
|
|
.offset_map
|
|
.cells_in_range(CharOffset::from(0usize), CharOffset::from(5usize));
|
|
assert_eq!(cells.len(), 2);
|
|
assert_eq!(cells[0].row, 0);
|
|
assert_eq!(cells[0].col, 0);
|
|
assert_eq!(cells[1].row, 0);
|
|
assert_eq!(cells[1].col, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn cells_in_range_cross_row() {
|
|
let table = test_laid_out_table();
|
|
let cells = table
|
|
.offset_map
|
|
.cells_in_range(CharOffset::from(3usize), CharOffset::from(9usize));
|
|
assert_eq!(cells.len(), 2);
|
|
assert_eq!(cells[0].row, 0);
|
|
assert_eq!(cells[0].col, 1);
|
|
assert_eq!(cells[1].row, 1);
|
|
assert_eq!(cells[1].col, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn cells_in_range_entire_table() {
|
|
let table = test_laid_out_table();
|
|
let cells = table
|
|
.offset_map
|
|
.cells_in_range(CharOffset::from(0usize), table.content_length());
|
|
assert_eq!(cells.len(), 6);
|
|
}
|
|
|
|
fn single_line_cell_layout(char_count: usize, line_height: f32, line_width: f32) -> CellLayout {
|
|
use galaxyui::text_layout::CaretPosition;
|
|
let mut carets = Vec::with_capacity(char_count);
|
|
let char_width = if char_count > 0 {
|
|
line_width / char_count as f32
|
|
} else {
|
|
0.0
|
|
};
|
|
for i in 0..char_count {
|
|
carets.push(CaretPosition {
|
|
start_offset: i,
|
|
last_offset: i,
|
|
position_in_line: i as f32 * char_width,
|
|
});
|
|
}
|
|
CellLayout {
|
|
line_heights: vec![line_height],
|
|
line_y_offsets: vec![0.0],
|
|
line_char_ranges: vec![CharOffset::from(0usize)..CharOffset::from(char_count)],
|
|
line_widths: vec![line_width],
|
|
line_caret_positions: vec![carets],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cell_layout_line_at_char_offset_single_line() {
|
|
let layout = single_line_cell_layout(5, 21.0, 50.0);
|
|
assert_eq!(
|
|
layout.line_at_char_offset(CharOffset::from(0usize)),
|
|
Some(0)
|
|
);
|
|
assert_eq!(
|
|
layout.line_at_char_offset(CharOffset::from(4usize)),
|
|
Some(0)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cell_layout_x_for_char_returns_proportional_position() {
|
|
let layout = single_line_cell_layout(4, 21.0, 40.0);
|
|
let x0 = layout.x_for_char_in_line(0, 0);
|
|
let x2 = layout.x_for_char_in_line(0, 2);
|
|
assert!(
|
|
x0 < x2,
|
|
"x at char 0 ({x0}) should be less than x at char 2 ({x2})"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cell_layout_x_past_end_returns_line_width() {
|
|
let layout = single_line_cell_layout(3, 21.0, 30.0);
|
|
let x = layout.x_for_char_in_line(0, 10);
|
|
assert_eq!(x, 30.0);
|
|
}
|
|
|
|
#[test]
|
|
fn coordinate_to_offset_with_cell_layouts() {
|
|
let mut table = test_laid_out_table();
|
|
table.cell_layouts = vec![
|
|
vec![
|
|
single_line_cell_layout(2, 21.0, 40.0),
|
|
single_line_cell_layout(2, 21.0, 40.0),
|
|
],
|
|
vec![
|
|
single_line_cell_layout(2, 21.0, 40.0),
|
|
single_line_cell_layout(2, 21.0, 40.0),
|
|
],
|
|
vec![
|
|
single_line_cell_layout(1, 21.0, 20.0),
|
|
single_line_cell_layout(1, 21.0, 20.0),
|
|
],
|
|
];
|
|
|
|
let offset_start = table.coordinate_to_offset(7.0, 1.0);
|
|
let cell_0_0 = table
|
|
.offset_map
|
|
.cell_range(0, 0)
|
|
.expect("cell (0,0) should exist");
|
|
assert!(
|
|
offset_start >= cell_0_0.start && offset_start <= cell_0_0.end,
|
|
"offset at (7,1) should be in cell (0,0)",
|
|
);
|
|
|
|
let offset_col1 = table.coordinate_to_offset(100.0, 30.0);
|
|
let cell_1_1 = table
|
|
.offset_map
|
|
.cell_range(1, 1)
|
|
.expect("cell (1,1) should exist");
|
|
assert!(
|
|
offset_col1 >= cell_1_1.start && offset_col1 <= cell_1_1.end,
|
|
"offset at (100,30) should be in cell (1,1)",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn table_selection_relative_range_starts_at_table_start() {
|
|
let table = test_laid_out_table();
|
|
let range = table_selection_relative_range(
|
|
&RenderedSelection::new(CharOffset::from(5usize), CharOffset::from(6usize)),
|
|
CharOffset::from(5usize),
|
|
&table,
|
|
)
|
|
.expect("selection should overlap table");
|
|
|
|
assert_eq!(range, CharOffset::from(0usize)..CharOffset::from(1usize));
|
|
}
|
|
|
|
#[test]
|
|
fn table_selection_relative_range_excludes_non_overlapping_selection() {
|
|
let table = test_laid_out_table();
|
|
let range = table_selection_relative_range(
|
|
&RenderedSelection::new(CharOffset::from(1usize), CharOffset::from(4usize)),
|
|
CharOffset::from(5usize),
|
|
&table,
|
|
);
|
|
|
|
assert!(range.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn table_cursor_relative_offset_starts_at_zero_for_first_table_character() {
|
|
let table = test_laid_out_table();
|
|
let relative_offset = table_cursor_relative_offset(
|
|
&RenderedSelection::new(CharOffset::from(5usize), CharOffset::from(5usize)),
|
|
CharOffset::from(5usize),
|
|
&table,
|
|
);
|
|
|
|
assert_eq!(relative_offset, Some(CharOffset::from(0usize)));
|
|
}
|
|
|
|
#[test]
|
|
fn table_cursor_relative_offset_excludes_cursor_before_table_start() {
|
|
let table = test_laid_out_table();
|
|
let relative_offset = table_cursor_relative_offset(
|
|
&RenderedSelection::new(CharOffset::from(4usize), CharOffset::from(4usize)),
|
|
CharOffset::from(5usize),
|
|
&table,
|
|
);
|
|
|
|
assert_eq!(relative_offset, None);
|
|
}
|
|
|
|
#[test]
|
|
fn table_scrollbar_uses_shared_overlay_geometry() {
|
|
let table = test_laid_out_table();
|
|
let scrollbar = table_scrollbar(
|
|
&table,
|
|
RectF::new(vec2f(10.0, 20.0), vec2f(90.0, table.height().as_f32())),
|
|
90.0.into_pixels(),
|
|
)
|
|
.expect("wide table should produce a horizontal scrollbar");
|
|
|
|
assert_eq!(scrollbar.track_bounds.height(), 12.0);
|
|
assert_eq!(scrollbar.track_bounds.width(), 90.0);
|
|
assert_eq!(scrollbar.thumb_bounds.height(), 8.0);
|
|
assert_eq!(scrollbar.thumb_bounds.origin_y(), 106.0);
|
|
assert_eq!(scrollbar.thumb_bounds.width(), 40.5);
|
|
}
|
|
|
|
#[test]
|
|
fn table_horizontal_scroll_delta_projects_trackpad_motion_without_vertical_jitter() {
|
|
assert_eq!(
|
|
table_horizontal_scroll_delta(vec2f(12.0, 4.0), true, false),
|
|
Some(12.0.into_pixels()),
|
|
);
|
|
assert_eq!(
|
|
table_horizontal_scroll_delta(vec2f(4.0, 12.0), true, false),
|
|
None,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn table_scrollbar_pointer_movement_matches_drag_and_gutter_behavior() {
|
|
let table = test_laid_out_table();
|
|
let viewport_width = 90.0.into_pixels();
|
|
let scroll_data = table_scroll_data(&table, viewport_width);
|
|
let scrollbar = table_scrollbar(
|
|
&table,
|
|
RectF::new(vec2f(10.0, 20.0), vec2f(90.0, table.height().as_f32())),
|
|
viewport_width,
|
|
)
|
|
.expect("wide table should produce a horizontal scrollbar");
|
|
|
|
assert_eq!(
|
|
scroll_delta_for_pointer_movement(20.0.into_pixels(), 38.0.into_pixels(), scroll_data),
|
|
(-40.0).into_pixels(),
|
|
);
|
|
assert_eq!(
|
|
scroll_delta_for_pointer_movement(
|
|
scrollbar.thumb_center_along(Axis::Horizontal),
|
|
75.25.into_pixels(),
|
|
scroll_data,
|
|
),
|
|
(-100.0).into_pixels(),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn table_scrollbar_drag_state_survives_renderable_recreation() {
|
|
let table = test_laid_out_table();
|
|
let viewport_width = 90.0.into_pixels();
|
|
table.start_scrollbar_drag(
|
|
20.0.into_pixels(),
|
|
table_scroll_data(&table, viewport_width),
|
|
);
|
|
|
|
let drag_state = table
|
|
.scrollbar_drag_state()
|
|
.expect("drag state should persist on the table");
|
|
let scroll_delta = scroll_delta_for_pointer_movement(
|
|
drag_state.start_position_x,
|
|
38.0.into_pixels(),
|
|
drag_state.scroll_data,
|
|
);
|
|
|
|
assert!(table.set_scroll_left(drag_state.start_scroll_left - scroll_delta, viewport_width));
|
|
assert_eq!(table.scroll_left(), 40.0.into_pixels());
|
|
assert!(table.end_scrollbar_drag());
|
|
}
|
|
|
|
#[test]
|
|
fn table_cursor_relative_offset_excludes_cursor_at_table_end() {
|
|
let table = test_laid_out_table();
|
|
let table_start = CharOffset::from(5usize);
|
|
let relative_offset = table_cursor_relative_offset(
|
|
&RenderedSelection::new(
|
|
table_start + table.content_length(),
|
|
table_start + table.content_length(),
|
|
),
|
|
table_start,
|
|
&table,
|
|
);
|
|
|
|
assert_eq!(relative_offset, None);
|
|
}
|