use crate::layout::flow::{FlowItem, FlowLayout};
use crate::types::{HitRegion, HitTestResult};
pub fn hit_test(flow: &FlowLayout, scroll_offset: f32, x: f32, y: f32) -> Option<HitTestResult> {
let doc_y = y + scroll_offset;
if let Some(result) = hit_test_in_frame(flow, doc_y, x) {
return Some(result);
}
if let Some(result) = hit_test_in_table(flow, doc_y, x) {
return Some(result);
}
let (block_id, block) = find_block_at_y(flow, doc_y)?;
hit_test_block(block_id, block, doc_y, x, 0.0, 0.0)
}
pub fn caret_rect(flow: &FlowLayout, scroll_offset: f32, position: usize) -> [f32; 4] {
for frame in flow.frames.values() {
if let Some(rect) = caret_rect_in_frame(frame, position, scroll_offset, 0.0, 0.0) {
return rect;
}
}
for table in flow.tables.values() {
for cell in &table.cell_layouts {
if cell.row >= table.row_ys.len() || cell.column >= table.column_xs.len() {
continue;
}
let offset_x = table.column_xs[cell.column];
let offset_y = table.y + table.row_ys[cell.row];
for block in &cell.blocks {
if let Some(rect) =
caret_rect_in_block(block, position, scroll_offset, offset_x, offset_y)
{
return rect;
}
}
}
}
for item in &flow.flow_order {
let bid = match item {
FlowItem::Block { block_id, .. } => *block_id,
_ => continue,
};
let block = match flow.blocks.get(&bid) {
Some(b) => b,
None => continue,
};
if let Some(rect) = caret_rect_in_block(block, position, scroll_offset, 0.0, 0.0) {
return rect;
}
}
[0.0, -scroll_offset, 2.0, 16.0]
}
use crate::layout::block::BlockLayout;
use crate::layout::line::LayoutLine;
fn hit_test_block(
block_id: usize,
block: &BlockLayout,
doc_y: f32,
x: f32,
offset_x: f32,
offset_y: f32,
) -> Option<HitTestResult> {
let content_top = offset_y + block.y;
let local_x = x - offset_x;
if local_x < block.left_margin {
return Some(HitTestResult {
position: block.position,
block_id,
offset_in_block: 0,
region: HitRegion::LeftMargin,
tooltip: None,
table_id: None,
});
}
let local_y = doc_y - content_top;
let line = match find_line_at_y(&block.lines, local_y) {
Some(l) => l,
None => {
if let Some(first_line) = block.lines.first()
&& local_y < first_line.y - first_line.ascent
{
return Some(HitTestResult {
position: block.position + first_line.char_range.start,
block_id,
offset_in_block: first_line.char_range.start,
region: HitRegion::BelowContent,
tooltip: None,
table_id: None,
});
}
if let Some(last_line) = block.lines.last() {
return Some(HitTestResult {
position: block.position + last_line.char_range.end,
block_id,
offset_in_block: last_line.char_range.end,
region: HitRegion::BelowContent,
tooltip: None,
table_id: None,
});
}
return Some(HitTestResult {
position: block.position,
block_id,
offset_in_block: 0,
region: HitRegion::BelowContent,
tooltip: None,
table_id: None,
});
}
};
let glyph_x = local_x - block.left_margin;
let (offset_in_block, region, tooltip) = find_position_in_line(line, glyph_x);
Some(HitTestResult {
position: block.position + offset_in_block,
block_id,
offset_in_block,
region,
tooltip,
table_id: None,
})
}
fn hit_test_in_frame(flow: &FlowLayout, doc_y: f32, x: f32) -> Option<HitTestResult> {
for item in &flow.flow_order {
let frame_id = match item {
FlowItem::Frame {
frame_id,
y,
height,
} if doc_y >= *y && doc_y < *y + *height => *frame_id,
_ => continue,
};
let frame = flow.frames.get(&frame_id)?;
let offset_x = frame.x + frame.content_x;
let offset_y = frame.y + frame.content_y;
let local_y = doc_y - offset_y;
if local_y < 0.0 || local_y >= frame.content_height {
continue;
}
for table in &frame.tables {
if local_y >= table.y
&& local_y < table.y + table.total_height
&& let Some(result) = hit_test_table_content(table, doc_y, x, offset_x, offset_y)
{
return Some(result);
}
}
for nested in &frame.frames {
let nested_content_y = offset_y + nested.y + nested.content_y;
let nested_local_y = doc_y - nested_content_y;
if nested_local_y >= 0.0
&& nested_local_y < nested.content_height
&& let Some(result) = hit_test_frame_content(nested, doc_y, x, offset_x, offset_y)
{
return Some(result);
}
}
for block in &frame.blocks {
let block_bottom = block.y + block.height;
if local_y >= block.y && local_y < block_bottom {
return hit_test_block(block.block_id, block, doc_y, x, offset_x, offset_y);
}
}
if let Some(block) = frame.blocks.last() {
return hit_test_block(block.block_id, block, doc_y, x, offset_x, offset_y);
}
}
None
}
fn hit_test_frame_content(
frame: &crate::layout::frame::FrameLayout,
doc_y: f32,
x: f32,
base_x: f32,
base_y: f32,
) -> Option<HitTestResult> {
let offset_x = base_x + frame.x + frame.content_x;
let offset_y = base_y + frame.y + frame.content_y;
let local_y = doc_y - offset_y;
for table in &frame.tables {
if local_y >= table.y
&& local_y < table.y + table.total_height
&& let Some(result) = hit_test_table_content(table, doc_y, x, offset_x, offset_y)
{
return Some(result);
}
}
for nested in &frame.frames {
let nested_content_y = offset_y + nested.y + nested.content_y;
let nested_local_y = doc_y - nested_content_y;
if nested_local_y >= 0.0
&& nested_local_y < nested.content_height
&& let Some(result) = hit_test_frame_content(nested, doc_y, x, offset_x, offset_y)
{
return Some(result);
}
}
for block in &frame.blocks {
let block_bottom = block.y + block.height;
if local_y >= block.y && local_y < block_bottom {
return hit_test_block(block.block_id, block, doc_y, x, offset_x, offset_y);
}
}
if let Some(block) = frame.blocks.last() {
return hit_test_block(block.block_id, block, doc_y, x, offset_x, offset_y);
}
None
}
fn hit_test_in_table(flow: &FlowLayout, doc_y: f32, x: f32) -> Option<HitTestResult> {
for item in &flow.flow_order {
let table_id = match item {
FlowItem::Table {
table_id,
y,
height,
} if doc_y >= *y && doc_y < *y + *height => *table_id,
_ => continue,
};
let table = flow.tables.get(&table_id)?;
return hit_test_table_content(table, doc_y, x, 0.0, 0.0);
}
None
}
fn hit_test_table_content(
table: &crate::layout::table::TableLayout,
doc_y: f32,
x: f32,
base_x: f32,
base_y: f32,
) -> Option<HitTestResult> {
let local_y = doc_y - base_y - table.y;
let local_x = x - base_x;
let row = find_table_row(table, local_y)?;
let col = find_table_column(table, local_x)?;
let cell = table
.cell_layouts
.iter()
.find(|c| c.row == row && c.column == col)?;
let cell_x = base_x + table.column_xs[col];
let cell_y = base_y + table.y + table.row_ys[row];
let tid = table.table_id;
let cell_local_y = doc_y - cell_y;
for block in &cell.blocks {
let block_bottom = block.y + block.height;
if cell_local_y >= block.y && cell_local_y < block_bottom {
return hit_test_block(block.block_id, block, doc_y, x, cell_x, cell_y).map(|r| {
HitTestResult {
table_id: Some(tid),
..r
}
});
}
}
if let Some(block) = cell.blocks.last() {
return hit_test_block(block.block_id, block, doc_y, x, cell_x, cell_y).map(|r| {
HitTestResult {
table_id: Some(tid),
..r
}
});
}
None
}
fn find_table_row(table: &crate::layout::table::TableLayout, local_y: f32) -> Option<usize> {
if table.row_ys.is_empty() {
return None;
}
for (r, &row_y) in table.row_ys.iter().enumerate() {
let row_top = row_y - table.cell_padding;
let row_bottom =
row_y + table.row_heights.get(r).copied().unwrap_or(0.0) + table.cell_padding;
if local_y >= row_top && local_y < row_bottom {
return Some(r);
}
}
let mut best_row = 0;
let mut best_dist = f32::MAX;
for (r, &row_y) in table.row_ys.iter().enumerate() {
let row_top = row_y - table.cell_padding;
let row_bottom =
row_y + table.row_heights.get(r).copied().unwrap_or(0.0) + table.cell_padding;
let row_mid = (row_top + row_bottom) / 2.0;
let dist = (local_y - row_mid).abs();
if dist < best_dist {
best_dist = dist;
best_row = r;
}
}
Some(best_row)
}
fn find_table_column(table: &crate::layout::table::TableLayout, local_x: f32) -> Option<usize> {
if table.column_xs.is_empty() {
return None;
}
for (c, &col_x) in table.column_xs.iter().enumerate() {
let col_left = col_x - table.cell_padding;
let col_right =
col_x + table.column_content_widths.get(c).copied().unwrap_or(0.0) + table.cell_padding;
if local_x >= col_left && local_x < col_right {
return Some(c);
}
}
let mut best_col = 0;
let mut best_dist = f32::MAX;
for (c, &col_x) in table.column_xs.iter().enumerate() {
let col_left = col_x - table.cell_padding;
let col_right =
col_x + table.column_content_widths.get(c).copied().unwrap_or(0.0) + table.cell_padding;
let col_mid = (col_left + col_right) / 2.0;
let dist = (local_x - col_mid).abs();
if dist < best_dist {
best_dist = dist;
best_col = c;
}
}
Some(best_col)
}
fn caret_rect_in_frame(
frame: &crate::layout::frame::FrameLayout,
position: usize,
scroll_offset: f32,
base_x: f32,
base_y: f32,
) -> Option<[f32; 4]> {
let fx = base_x + frame.x + frame.content_x;
let fy = base_y + frame.y + frame.content_y;
for table in &frame.tables {
for cell in &table.cell_layouts {
if cell.row >= table.row_ys.len() || cell.column >= table.column_xs.len() {
continue;
}
let offset_x = fx + table.column_xs[cell.column];
let offset_y = fy + table.y + table.row_ys[cell.row];
for block in &cell.blocks {
if let Some(rect) =
caret_rect_in_block(block, position, scroll_offset, offset_x, offset_y)
{
return Some(rect);
}
}
}
}
for block in &frame.blocks {
if let Some(rect) = caret_rect_in_block(block, position, scroll_offset, fx, fy) {
return Some(rect);
}
}
for nested in &frame.frames {
if let Some(rect) = caret_rect_in_frame(nested, position, scroll_offset, fx, fy) {
return Some(rect);
}
}
None
}
fn caret_rect_in_block(
block: &BlockLayout,
position: usize,
scroll_offset: f32,
offset_x: f32,
offset_y: f32,
) -> Option<[f32; 4]> {
let block_end = block.position + block.lines.last().map(|l| l.char_range.end).unwrap_or(0);
if position < block.position || position > block_end {
return None;
}
let offset_in_block = position - block.position;
for line in &block.lines {
if offset_in_block < line.char_range.start {
continue;
}
if offset_in_block > line.char_range.end {
continue;
}
let caret_x = line.x_for_offset(offset_in_block) + block.left_margin + offset_x;
let caret_y = offset_y + block.y + line.y - line.ascent - scroll_offset;
let caret_height = line.line_height;
return Some([caret_x, caret_y, 2.0, caret_height]);
}
None
}
fn find_block_at_y(flow: &FlowLayout, doc_y: f32) -> Option<(usize, &BlockLayout)> {
if flow.flow_order.is_empty() {
return None;
}
let idx = flow.flow_order.partition_point(|item| {
let y = match item {
FlowItem::Block { y, .. } | FlowItem::Table { y, .. } | FlowItem::Frame { y, .. } => *y,
};
y <= doc_y
});
let start = idx.saturating_sub(1);
let end = (idx + 1).min(flow.flow_order.len());
for i in start..end {
if let FlowItem::Block {
block_id,
y,
height,
} = &flow.flow_order[i]
&& doc_y >= *y
&& doc_y < *y + *height
&& let Some(block) = flow.blocks.get(block_id)
{
return Some((*block_id, block));
}
}
if idx == 0 {
for item in &flow.flow_order {
if let FlowItem::Block { block_id, .. } = item
&& let Some(block) = flow.blocks.get(block_id)
{
return Some((*block_id, block));
}
}
}
for item in flow.flow_order.iter().rev() {
if let FlowItem::Block { block_id, .. } = item
&& let Some(block) = flow.blocks.get(block_id)
{
return Some((*block_id, block));
}
}
None
}
fn find_line_at_y(lines: &[LayoutLine], local_y: f32) -> Option<&LayoutLine> {
for line in lines {
let line_top = line.y - line.ascent;
let line_bottom = line_top + line.line_height;
if local_y >= line_top && local_y < line_bottom {
return Some(line);
}
}
None
}
fn find_position_in_line(line: &LayoutLine, local_x: f32) -> (usize, HitRegion, Option<String>) {
for run in &line.runs {
if let Some(ref name) = run.shaped_run.image_name {
let img_end = run.x + run.shaped_run.advance_width;
if local_x < img_end {
let offset = run
.shaped_run
.glyphs
.first()
.map(|g| g.cluster as usize)
.unwrap_or(line.char_range.start);
return (
offset,
HitRegion::Image { name: name.clone() },
run.decorations.tooltip.clone(),
);
}
continue;
}
let mut glyph_x = run.x;
for glyph in &run.shaped_run.glyphs {
let glyph_mid = glyph_x + glyph.x_advance / 2.0;
if local_x < glyph_mid {
let offset = glyph.cluster as usize;
let tooltip = run.decorations.tooltip.clone();
if run.decorations.is_link {
return (
offset,
HitRegion::Link {
href: run.decorations.anchor_href.clone().unwrap_or_default(),
},
tooltip,
);
}
return (offset, HitRegion::Text, tooltip);
}
glyph_x += glyph.x_advance;
}
}
(line.char_range.end, HitRegion::PastLineEnd, None)
}