use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::widgets::{Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState};
use crate::ui::theme::Theme;
pub struct HorizontalScrollParams {
pub position: usize,
pub viewport_size: usize,
pub total_items: usize,
}
pub fn render_horizontal_scroll_indicator(
frame: &mut Frame,
area: Rect,
params: HorizontalScrollParams,
) {
if params.total_items <= params.viewport_size {
return;
}
let can_scroll_left = params.position > 0;
let can_scroll_right = params.position + params.viewport_size < params.total_items;
if !can_scroll_left && !can_scroll_right {
return;
}
let left_margin: u16 = 1;
let available_width = area.width.saturating_sub(left_margin) as usize;
if available_width < 15 {
return;
}
let scrollable_range = params.total_items.saturating_sub(params.viewport_size);
let percentage = if scrollable_range > 0 {
(params.position * 100) / scrollable_range
} else {
0
};
let position_text = format!("col {:>3}%", percentage.min(100));
let fixed_parts_len = position_text.len() + 1;
let scrollbar_width = available_width.saturating_sub(fixed_parts_len).max(5);
use ratatui::text::{Line, Span};
let text_style = Style::default().fg(Theme::SCROLLBAR_ACTIVE);
let position_span = Span::styled(format!("{} ", position_text), text_style);
let text_line = Line::from(vec![position_span]);
let text_area = Rect {
x: area.x + left_margin,
y: area.y + area.height.saturating_sub(1),
width: (text_line.width() as u16).min(area.width),
height: 1,
};
frame.render_widget(Paragraph::new(text_line), text_area);
let scrollbar_area = Rect {
x: text_area.x + text_area.width,
y: text_area.y,
width: scrollbar_width as u16,
height: 1,
};
let arrow_active = Style::default().fg(Theme::SCROLLBAR_ACTIVE);
let arrow_inactive = Style::default().fg(Theme::SCROLLBAR_INACTIVE);
let scrollbar = Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("═")
.track_symbol(Some("─"))
.begin_symbol(Some("◀︎"))
.end_symbol(Some("▶︎"))
.thumb_style(Style::default().fg(Theme::SCROLLBAR_ACTIVE))
.track_style(Style::default().fg(Theme::SCROLLBAR_INACTIVE))
.begin_style(if can_scroll_left {
arrow_active
} else {
arrow_inactive
})
.end_style(if can_scroll_right {
arrow_active
} else {
arrow_inactive
});
let scrollable_range = params.total_items.saturating_sub(params.viewport_size);
let mut scrollbar_state = ScrollbarState::default()
.content_length(scrollable_range)
.viewport_content_length(0) .position(params.position);
frame.render_stateful_widget(scrollbar, scrollbar_area, &mut scrollbar_state);
}
pub struct VerticalScrollParams {
pub position: usize,
pub viewport_size: usize,
pub total_items: usize,
}
pub fn render_vertical_scroll_indicator_bar(
frame: &mut Frame,
area: Rect,
params: VerticalScrollParams,
) {
if params.total_items <= params.viewport_size {
return;
}
let can_scroll_up = params.position > 0;
let can_scroll_down = params.position + params.viewport_size < params.total_items;
if !can_scroll_up && !can_scroll_down {
return;
}
if area.height < 3 {
return;
}
let scrollable_range = params.total_items.saturating_sub(params.viewport_size);
let percentage = if scrollable_range > 0 {
(params.position * 100) / scrollable_range
} else {
0
};
let position_text = format!("row {:>3}%", percentage.min(100));
let text_style = Style::default().fg(Theme::SCROLLBAR_ACTIVE);
let text_width = position_text.len() as u16;
let text_area = Rect {
x: area.x + area.width.saturating_sub(text_width),
y: area.y + area.height.saturating_sub(1),
width: text_width,
height: 1,
};
frame.render_widget(Paragraph::new(position_text).style(text_style), text_area);
let scrollbar_height = area.height.saturating_sub(1); if scrollbar_height < 3 {
return;
}
let scrollbar_area = Rect {
x: area.x + area.width.saturating_sub(1),
y: area.y,
width: 1,
height: scrollbar_height,
};
let arrow_active = Style::default().fg(Theme::SCROLLBAR_ACTIVE);
let arrow_inactive = Style::default().fg(Theme::SCROLLBAR_INACTIVE);
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.thumb_symbol("┃")
.track_symbol(Some("│"))
.begin_symbol(Some("▲"))
.end_symbol(Some("▼"))
.thumb_style(Style::default().fg(Theme::SCROLLBAR_ACTIVE))
.track_style(Style::default().fg(Theme::SCROLLBAR_INACTIVE))
.begin_style(if can_scroll_up {
arrow_active
} else {
arrow_inactive
})
.end_style(if can_scroll_down {
arrow_active
} else {
arrow_inactive
});
let scrollable_range = params.total_items.saturating_sub(params.viewport_size);
let mut scrollbar_state = ScrollbarState::default()
.content_length(scrollable_range)
.viewport_content_length(0) .position(params.position);
frame.render_stateful_widget(scrollbar, scrollbar_area, &mut scrollbar_state);
}
pub fn clamp_scroll_offset(
scroll_offset: usize,
viewport_size: usize,
total_items: usize,
) -> usize {
scroll_offset.min(total_items.saturating_sub(viewport_size))
}
#[cfg(test)]
mod tests {
use super::clamp_scroll_offset;
#[test]
fn clamp_returns_zero_when_content_fits() {
assert_eq!(clamp_scroll_offset(100, 10, 5), 0);
}
#[test]
fn clamp_returns_zero_when_exact_fit() {
assert_eq!(clamp_scroll_offset(5, 10, 10), 0);
}
#[test]
fn clamp_preserves_offset_within_range() {
assert_eq!(clamp_scroll_offset(50, 10, 100), 50);
}
#[test]
fn clamp_caps_offset_exceeding_max() {
assert_eq!(clamp_scroll_offset(100, 10, 20), 10);
}
}