use crate::model::buffer::Buffer;
use crate::primitives::line_wrapping::{wrap_line, WrapConfig};
fn estimated_gutter_width(buffer: &Buffer) -> usize {
let line_count = buffer.line_count().unwrap_or(1);
let digits = (line_count as f64).log10().floor() as usize + 1;
1 + digits.max(4) + 3
}
fn build_visual_row_map(
buffer: &mut Buffer,
viewport_width: usize,
) -> (Vec<(usize, usize)>, usize) {
let gutter_width = estimated_gutter_width(buffer);
let wrap_config = WrapConfig::new(viewport_width, gutter_width, true, true);
let mut total_visual_rows = 0;
let mut visual_row_positions: Vec<(usize, usize)> = Vec::new();
let mut iter = buffer.line_iterator(0, 80);
while let Some((line_start, content)) = iter.next_line() {
let line_content = content.trim_end_matches(['\n', '\r']).to_string();
let segments = wrap_line(&line_content, &wrap_config);
let visual_rows_in_line = segments.len().max(1);
for offset in 0..visual_rows_in_line {
visual_row_positions.push((line_start, offset));
}
total_visual_rows += visual_rows_in_line;
}
(visual_row_positions, total_visual_rows)
}
fn position_at(visual_row_positions: &[(usize, usize)], target_row: usize) -> (usize, usize) {
if target_row < visual_row_positions.len() {
visual_row_positions[target_row]
} else {
visual_row_positions.last().copied().unwrap_or((0, 0))
}
}
pub(crate) fn scrollbar_jump_visual(
buffer: &mut Buffer,
ratio: f64,
viewport_height: usize,
viewport_width: usize,
) -> (usize, usize) {
if buffer.len() == 0 || viewport_height == 0 {
return (0, 0);
}
let (visual_row_positions, total_visual_rows) = build_visual_row_map(buffer, viewport_width);
if total_visual_rows == 0 {
return (0, 0);
}
let max_scroll_row = total_visual_rows.saturating_sub(viewport_height);
if max_scroll_row == 0 {
return (0, 0);
}
let target_row = (ratio * max_scroll_row as f64).round() as usize;
let target_row = target_row.min(max_scroll_row);
position_at(&visual_row_positions, target_row)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn scrollbar_drag_relative_visual(
buffer: &mut Buffer,
current_row: u16,
scrollbar_y: u16,
scrollbar_height: usize,
drag_start_row: u16,
drag_start_top_byte: usize,
drag_start_view_line_offset: usize,
viewport_height: usize,
viewport_width: usize,
) -> (usize, usize) {
if buffer.len() == 0 || viewport_height == 0 || scrollbar_height <= 1 {
return (0, 0);
}
let (visual_row_positions, total_visual_rows) = build_visual_row_map(buffer, viewport_width);
if total_visual_rows == 0 {
return (0, 0);
}
let max_scroll_row = total_visual_rows.saturating_sub(viewport_height);
if max_scroll_row == 0 {
return (0, 0);
}
let line_start_visual_row = visual_row_positions
.iter()
.position(|(byte, _)| *byte >= drag_start_top_byte)
.unwrap_or(0);
let start_visual_row =
(line_start_visual_row + drag_start_view_line_offset).min(max_scroll_row);
let thumb_size_raw = (viewport_height as f64 / total_visual_rows as f64
* scrollbar_height as f64)
.ceil() as usize;
let max_thumb_size = (scrollbar_height as f64 * 0.8).floor() as usize;
let thumb_size = thumb_size_raw
.max(1)
.min(max_thumb_size)
.min(scrollbar_height);
let max_thumb_start = scrollbar_height.saturating_sub(thumb_size);
let start_scroll_ratio = start_visual_row as f64 / max_scroll_row as f64;
let thumb_row_at_start = scrollbar_y as f64 + start_scroll_ratio * max_thumb_start as f64;
let click_offset = drag_start_row as f64 - thumb_row_at_start;
let target_thumb_row = current_row as f64 - click_offset;
let target_scroll_ratio = if max_thumb_start > 0 {
((target_thumb_row - scrollbar_y as f64) / max_thumb_start as f64).clamp(0.0, 1.0)
} else {
0.0
};
let target_row = (target_scroll_ratio * max_scroll_row as f64).round() as usize;
let target_row = target_row.min(max_scroll_row);
position_at(&visual_row_positions, target_row)
}