use ratatui::layout::Rect;
use super::popup::{Popup, PopupContent, PopupManager};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PopupClickResult {
Link { url: String },
ListItem { popup_idx: usize, item_idx: usize },
TextContent {
popup_idx: usize,
line: usize,
col: usize,
},
Scrollbar {
popup_idx: usize,
target_scroll: i32,
},
Background,
Outside,
}
#[derive(Debug, Clone)]
pub struct PopupLayoutInfo {
pub popup_idx: usize,
pub outer_rect: Rect,
pub inner_rect: Rect,
pub scroll_offset: usize,
pub num_items: usize,
pub scrollbar_rect: Option<Rect>,
pub total_lines: usize,
}
pub struct PopupHitTester<'a> {
layouts: &'a [PopupLayoutInfo],
popups: &'a PopupManager,
}
impl<'a> PopupHitTester<'a> {
pub fn new(layouts: &'a [PopupLayoutInfo], popups: &'a PopupManager) -> Self {
Self { layouts, popups }
}
pub fn is_over_popup(&self, col: u16, row: u16) -> bool {
if !self.popups.is_visible() {
return false;
}
self.layouts.iter().any(|layout| {
col >= layout.outer_rect.x
&& col < layout.outer_rect.x + layout.outer_rect.width
&& row >= layout.outer_rect.y
&& row < layout.outer_rect.y + layout.outer_rect.height
})
}
pub fn is_over_transient_popup(&self, col: u16, row: u16) -> bool {
let has_transient = self.popups.top().is_some_and(|p| p.transient);
if !has_transient {
return false;
}
self.is_over_popup(col, row)
}
pub fn hit_test_click(&self, col: u16, row: u16) -> PopupClickResult {
for layout in self.layouts.iter().rev() {
if let Some(sb_rect) = &layout.scrollbar_rect {
if col >= sb_rect.x
&& col < sb_rect.x + sb_rect.width
&& row >= sb_rect.y
&& row < sb_rect.y + sb_rect.height
{
let track_height = sb_rect.height as usize;
let visible_lines = layout.inner_rect.height as usize;
if track_height > 0 && layout.total_lines > visible_lines {
let relative_row = (row - sb_rect.y) as usize;
let max_scroll = layout.total_lines.saturating_sub(visible_lines);
let target_scroll = if track_height > 1 {
((relative_row * max_scroll) / (track_height - 1)) as i32
} else {
0
};
return PopupClickResult::Scrollbar {
popup_idx: layout.popup_idx,
target_scroll,
};
}
}
}
if col >= layout.inner_rect.x
&& col < layout.inner_rect.x + layout.inner_rect.width
&& row >= layout.inner_rect.y
&& row < layout.inner_rect.y + layout.inner_rect.height
{
let relative_col = (col - layout.inner_rect.x) as usize;
let relative_row = (row - layout.inner_rect.y) as usize;
if let Some(popup) = self.popups.get(layout.popup_idx) {
if let Some(url) = popup.link_at_position(relative_col, relative_row) {
return PopupClickResult::Link { url };
}
}
if layout.num_items > 0 {
let item_idx = layout.scroll_offset + relative_row;
if item_idx < layout.num_items {
return PopupClickResult::ListItem {
popup_idx: layout.popup_idx,
item_idx,
};
}
}
if let Some(popup) = self.popups.get(layout.popup_idx) {
if matches!(
popup.content,
PopupContent::Text(_) | PopupContent::Markdown(_)
) {
return PopupClickResult::TextContent {
popup_idx: layout.popup_idx,
line: layout.scroll_offset + relative_row,
col: relative_col,
};
}
}
return PopupClickResult::Background;
}
if col >= layout.outer_rect.x
&& col < layout.outer_rect.x + layout.outer_rect.width
&& row >= layout.outer_rect.y
&& row < layout.outer_rect.y + layout.outer_rect.height
{
return PopupClickResult::Background;
}
}
PopupClickResult::Outside
}
pub fn hover_target(&self, col: u16, row: u16) -> Option<(usize, usize)> {
for layout in self.layouts.iter().rev() {
if col >= layout.inner_rect.x
&& col < layout.inner_rect.x + layout.inner_rect.width
&& row >= layout.inner_rect.y
&& row < layout.inner_rect.y + layout.inner_rect.height
&& layout.num_items > 0
{
let relative_row = (row - layout.inner_rect.y) as usize;
let item_idx = layout.scroll_offset + relative_row;
if item_idx < layout.num_items {
return Some((layout.popup_idx, item_idx));
}
}
}
None
}
pub fn content_position(&self, col: u16, row: u16) -> Option<(usize, usize, usize)> {
for layout in self.layouts.iter().rev() {
if col >= layout.inner_rect.x
&& col < layout.inner_rect.x + layout.inner_rect.width
&& row >= layout.inner_rect.y
&& row < layout.inner_rect.y + layout.inner_rect.height
{
let relative_col = (col - layout.inner_rect.x) as usize;
let relative_row = (row - layout.inner_rect.y) as usize;
let line = layout.scroll_offset + relative_row;
return Some((layout.popup_idx, line, relative_col));
}
}
None
}
}
pub fn popup_areas_to_layout_info(
popup_areas: &[(usize, Rect, Rect, usize, usize, Option<Rect>, usize)],
) -> Vec<PopupLayoutInfo> {
popup_areas
.iter()
.map(
|(
popup_idx,
outer_rect,
inner_rect,
scroll_offset,
num_items,
scrollbar_rect,
total_lines,
)| {
PopupLayoutInfo {
popup_idx: *popup_idx,
outer_rect: *outer_rect,
inner_rect: *inner_rect,
scroll_offset: *scroll_offset,
num_items: *num_items,
scrollbar_rect: *scrollbar_rect,
total_lines: *total_lines,
}
},
)
.collect()
}
pub fn handle_popup_selection_drag(
popup: &mut Popup,
layout: &PopupLayoutInfo,
col: u16,
row: u16,
) {
if col >= layout.inner_rect.x
&& col < layout.inner_rect.x + layout.inner_rect.width
&& row >= layout.inner_rect.y
&& row < layout.inner_rect.y + layout.inner_rect.height
{
let relative_col = (col - layout.inner_rect.x) as usize;
let relative_row = (row - layout.inner_rect.y) as usize;
let line = layout.scroll_offset + relative_row;
popup.extend_selection(line, relative_col);
}
}