use rdom_core::{Dom, Position};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::ext::TuiExt;
use crate::layout::LayoutRect;
use crate::render::inline::InlineFragment;
use super::nearest::InlineTarget;
pub(crate) fn resolve_in_target(
dom: &Dom<TuiExt>,
target: InlineTarget,
x: u16,
y: u16,
) -> Option<Position> {
let (inline_layout, content) = target.layout_and_rect(dom)?;
match fragment_at_layout(inline_layout, content, x, y) {
Some(fragment) => {
let cell_offset_in_frag = (x as i32 - content.x - fragment.x as i32).max(0) as u16;
let bytes_into_text = cells_to_bytes(&fragment.text, cell_offset_in_frag);
Some(Position::new(
fragment.text_node,
fragment.source_byte_offset + bytes_into_text,
))
}
None => clamp_to_line_layout(inline_layout, content, x, y),
}
}
fn clamp_to_line_layout(
layout: &crate::render::inline::InlineLayout,
content: crate::layout::LayoutRect,
x: u16,
y: u16,
) -> Option<Position> {
if layout.lines.is_empty() {
return None;
}
let (line_idx, overshoot_up, overshoot_down) = if (y as i32) < content.y {
(0, true, false)
} else if (y as i32) >= content.y + content.height as i32 {
(layout.lines.len() - 1, false, true)
} else {
let raw = (y as i32 - content.y) as usize;
(raw.min(layout.lines.len() - 1), false, false)
};
let target_line = &layout.lines[line_idx];
if target_line.fragments.is_empty() {
for line in layout.lines.iter().rev() {
if let Some(frag) = line.fragments.last() {
return Some(Position::new(
frag.text_node,
frag.source_byte_offset + frag.text.len(),
));
}
}
return None;
}
let first = target_line.fragments.first().unwrap();
let last = target_line.fragments.last().unwrap();
if overshoot_up {
return Some(Position::new(first.text_node, first.source_byte_offset));
}
if overshoot_down {
return Some(Position::new(
last.text_node,
last.source_byte_offset + last.text.len(),
));
}
let line_left = content.x + first.x as i32;
let line_right = content.x + last.x as i32 + last.width as i32;
if (x as i32) < line_left {
Some(Position::new(first.text_node, first.source_byte_offset))
} else if (x as i32) >= line_right {
Some(Position::new(
last.text_node,
last.source_byte_offset + last.text.len(),
))
} else {
Some(Position::new(
last.text_node,
last.source_byte_offset + last.text.len(),
))
}
}
fn fragment_at_layout(
layout: &crate::render::inline::InlineLayout,
content: LayoutRect,
x: u16,
y: u16,
) -> Option<&InlineFragment> {
let line_index = y as i32 - content.y;
if line_index < 0 || line_index as usize >= layout.lines.len() {
return None;
}
let line = &layout.lines[line_index as usize];
let x_local_i = x as i32 - content.x;
if x_local_i < 0 {
return None;
}
let x_local = x_local_i as u16;
line.fragments
.iter()
.find(|&fragment| x_local >= fragment.x && x_local < fragment.x + fragment.width)
.map(|v| v as _)
}
fn cells_to_bytes(text: &str, target_cells: u16) -> usize {
let mut consumed_cells: u16 = 0;
for (idx, g) in text.grapheme_indices(true) {
let w = UnicodeWidthStr::width(g) as u16;
if target_cells < consumed_cells.saturating_add(w) {
return idx;
}
consumed_cells = consumed_cells.saturating_add(w);
}
text.len()
}