use crate::components::{Box as RnkBox, Text};
use crate::core::{BorderStyle, Color, Element, FlexDirection, Overflow};
use super::keymap::{ViewportAction, ViewportKeyMap};
use super::state::ViewportState;
#[derive(Debug, Clone)]
pub struct ViewportStyle {
pub border: Option<BorderStyle>,
pub border_color: Option<Color>,
pub background: Option<Color>,
pub text_color: Option<Color>,
pub line_numbers: bool,
pub line_number_color: Option<Color>,
pub line_number_width: Option<usize>,
pub scrollbar: bool,
pub scrollbar_color: Option<Color>,
pub scrollbar_track_color: Option<Color>,
}
impl Default for ViewportStyle {
fn default() -> Self {
Self {
border: None,
border_color: None,
background: None,
text_color: None,
line_numbers: false,
line_number_color: Some(Color::BrightBlack),
line_number_width: None,
scrollbar: false,
scrollbar_color: Some(Color::BrightBlack),
scrollbar_track_color: None,
}
}
}
impl ViewportStyle {
pub fn new() -> Self {
Self::default()
}
pub fn border(mut self, style: BorderStyle) -> Self {
self.border = Some(style);
self
}
pub fn border_color(mut self, color: Color) -> Self {
self.border_color = Some(color);
self
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn text_color(mut self, color: Color) -> Self {
self.text_color = Some(color);
self
}
pub fn line_numbers(mut self, show: bool) -> Self {
self.line_numbers = show;
self
}
pub fn line_number_color(mut self, color: Color) -> Self {
self.line_number_color = Some(color);
self
}
pub fn scrollbar(mut self, show: bool) -> Self {
self.scrollbar = show;
self
}
pub fn scrollbar_color(mut self, color: Color) -> Self {
self.scrollbar_color = Some(color);
self
}
}
#[derive(Debug, Clone)]
pub struct Viewport<'a> {
state: &'a ViewportState,
style: ViewportStyle,
keymap: ViewportKeyMap,
focused: bool,
}
impl<'a> Viewport<'a> {
pub fn new(state: &'a ViewportState) -> Self {
Self {
state,
style: ViewportStyle::default(),
keymap: ViewportKeyMap::default(),
focused: true,
}
}
pub fn style(mut self, style: ViewportStyle) -> Self {
self.style = style;
self
}
pub fn keymap(mut self, keymap: ViewportKeyMap) -> Self {
self.keymap = keymap;
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn line_numbers(mut self, show: bool) -> Self {
self.style.line_numbers = show;
self
}
pub fn scrollbar(mut self, show: bool) -> Self {
self.style.scrollbar = show;
self
}
pub fn border(mut self, style: BorderStyle) -> Self {
self.style.border = Some(style);
self
}
pub fn border_color(mut self, color: Color) -> Self {
self.style.border_color = Some(color);
self
}
pub fn background(mut self, color: Color) -> Self {
self.style.background = Some(color);
self
}
pub fn get_keymap(&self) -> &ViewportKeyMap {
&self.keymap
}
pub fn into_element(self) -> Element {
let height = self.state.height();
let width = self.state.width();
let mut container = RnkBox::new()
.flex_direction(FlexDirection::Column)
.height(height as i32)
.width(width as i32)
.overflow_y(Overflow::Hidden);
if let Some(border) = self.style.border {
container = container.border_style(border);
}
if let Some(color) = self.style.border_color {
container = container.border_color(color);
}
if let Some(color) = self.style.background {
container = container.background(color);
}
let line_num_width = if self.style.line_numbers {
self.style.line_number_width.unwrap_or_else(|| {
let total = self.state.total_line_count();
if total == 0 {
1
} else {
(total as f64).log10().floor() as usize + 1
}
})
} else {
0
};
let y_offset = self.state.y_offset();
for (i, line) in self.state.visible_lines().enumerate() {
let global_line_num = y_offset + i + 1;
let line_element = if self.style.line_numbers {
let num_str = format!("{:>width$} ", global_line_num, width = line_num_width);
let mut num_text = Text::new(&num_str);
if let Some(color) = self.style.line_number_color {
num_text = num_text.color(color);
}
num_text = num_text.dim();
let mut content_text = Text::new(line);
if let Some(color) = self.style.text_color {
content_text = content_text.color(color);
}
RnkBox::new()
.flex_direction(FlexDirection::Row)
.child(num_text.into_element())
.child(content_text.into_element())
.into_element()
} else {
let mut text = Text::new(line);
if let Some(color) = self.style.text_color {
text = text.color(color);
}
text.into_element()
};
container = container.child(line_element);
}
if self.style.scrollbar && !self.state.fits_in_viewport() {
let scrollbar = self.render_scrollbar();
return RnkBox::new()
.flex_direction(FlexDirection::Row)
.child(container.into_element())
.child(scrollbar)
.into_element();
}
container.into_element()
}
fn render_scrollbar(&self) -> Element {
let height = self.state.height();
let total_lines = self.state.total_line_count();
if total_lines == 0 || height == 0 {
return RnkBox::new().into_element();
}
let thumb_size = ((height as f64 / total_lines as f64) * height as f64)
.max(1.0)
.min(height as f64) as usize;
let thumb_pos = (self.state.scroll_percent() * (height - thumb_size) as f64) as usize;
let mut scrollbar_box = RnkBox::new().flex_direction(FlexDirection::Column).width(1);
for i in 0..height {
let is_thumb = i >= thumb_pos && i < thumb_pos + thumb_size;
let char = if is_thumb { "█" } else { "░" };
let mut text = Text::new(char);
if is_thumb {
if let Some(color) = self.style.scrollbar_color {
text = text.color(color);
}
} else if let Some(color) = self.style.scrollbar_track_color {
text = text.color(color);
}
scrollbar_box = scrollbar_box.child(text.into_element());
}
scrollbar_box.into_element()
}
}
pub fn handle_viewport_input(
state: &mut ViewportState,
input: &str,
key: &crate::hooks::Key,
keymap: &ViewportKeyMap,
) -> bool {
if let Some(action) = keymap.match_action(input, key) {
apply_viewport_action(state, action);
true
} else {
false
}
}
pub fn apply_viewport_action(state: &mut ViewportState, action: ViewportAction) {
match action {
ViewportAction::ScrollUp => state.scroll_up(1),
ViewportAction::ScrollDown => state.scroll_down(1),
ViewportAction::PageUp => state.page_up(),
ViewportAction::PageDown => state.page_down(),
ViewportAction::HalfPageUp => state.half_page_up(),
ViewportAction::HalfPageDown => state.half_page_down(),
ViewportAction::GotoTop => state.goto_top(),
ViewportAction::GotoBottom => state.goto_bottom(),
ViewportAction::ScrollLeft => state.scroll_left(1),
ViewportAction::ScrollRight => state.scroll_right(1),
ViewportAction::GotoLeft => state.goto_left(),
ViewportAction::GotoRight => state.goto_right(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_viewport_creation() {
let mut state = ViewportState::new(80, 10);
state.set_content("line1\nline2\nline3");
let viewport = Viewport::new(&state);
let element = viewport.into_element();
assert_eq!(element.children.len(), 3);
}
#[test]
fn test_viewport_with_line_numbers() {
let mut state = ViewportState::new(80, 10);
state.set_content("a\nb\nc");
let viewport = Viewport::new(&state).line_numbers(true);
let element = viewport.into_element();
assert_eq!(element.children.len(), 3);
}
#[test]
fn test_viewport_with_scrollbar() {
let mut state = ViewportState::new(80, 5);
state.set_content("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
let viewport = Viewport::new(&state).scrollbar(true);
let element = viewport.into_element();
assert_eq!(element.children.len(), 2);
}
}