use std::ops::Range;
use std::sync::Arc;
use tracing::debug;
use crate::data::data_view::DataView;
use crate::data::datatable::DataRow;
use crate::ui::viewport::column_width_calculator::{
COLUMN_PADDING, MAX_COL_WIDTH, MAX_COL_WIDTH_DATA_FOCUS, MIN_COL_WIDTH,
};
use crate::ui::viewport::{ColumnPackingMode, ColumnWidthCalculator};
#[derive(Debug, Clone)]
pub struct NavigationResult {
pub column_position: usize,
pub scroll_offset: usize,
pub description: String,
pub viewport_changed: bool,
}
#[derive(Debug, Clone)]
pub struct RowNavigationResult {
pub row_position: usize,
pub row_scroll_offset: usize,
pub description: String,
pub viewport_changed: bool,
}
#[derive(Debug, Clone)]
pub struct ColumnReorderResult {
pub new_column_position: usize,
pub description: String,
pub success: bool,
}
#[derive(Debug, Clone)]
pub struct ColumnOperationResult {
pub success: bool,
pub description: String,
pub updated_dataview: Option<DataView>,
pub new_column_position: Option<usize>,
pub new_viewport: Option<std::ops::Range<usize>>,
pub affected_count: Option<usize>,
}
impl ColumnOperationResult {
pub fn failure(description: impl Into<String>) -> Self {
Self {
success: false,
description: description.into(),
updated_dataview: None,
new_column_position: None,
new_viewport: None,
affected_count: None,
}
}
pub fn success(description: impl Into<String>) -> Self {
Self {
success: true,
description: description.into(),
updated_dataview: None,
new_column_position: None,
new_viewport: None,
affected_count: None,
}
}
}
const TABLE_CHROME_ROWS: usize = 3;
const TABLE_BORDER_WIDTH: u16 = 4;
pub struct ViewportManager {
dataview: Arc<DataView>,
viewport_rows: Range<usize>,
viewport_cols: Range<usize>,
terminal_width: u16,
terminal_height: u16,
width_calculator: ColumnWidthCalculator,
visible_row_cache: Vec<usize>,
cache_signature: u64,
cache_dirty: bool,
crosshair_row: usize,
crosshair_col: usize,
cursor_lock: bool,
cursor_lock_position: Option<usize>,
viewport_lock: bool,
viewport_lock_boundaries: Option<std::ops::Range<usize>>,
}
impl ViewportManager {
#[must_use]
pub fn get_viewport_range(&self) -> std::ops::Range<usize> {
self.viewport_cols.clone()
}
#[must_use]
pub fn get_viewport_rows(&self) -> std::ops::Range<usize> {
self.viewport_rows.clone()
}
pub fn set_crosshair(&mut self, row: usize, col: usize) {
self.crosshair_row = row;
self.crosshair_col = col;
debug!(target: "viewport_manager",
"Crosshair set to visual position: row={}, col={}", row, col);
}
pub fn set_crosshair_row(&mut self, row: usize) {
let total_rows = self.dataview.row_count();
let clamped_row = row.min(total_rows.saturating_sub(1));
self.crosshair_row = clamped_row;
if self.viewport_lock {
debug!(target: "viewport_manager",
"Crosshair row set to: {} (viewport locked, no scroll adjustment)",
clamped_row);
return;
}
let viewport_height = self.viewport_rows.len();
let mut viewport_changed = false;
if clamped_row < self.viewport_rows.start {
self.viewport_rows = clamped_row..(clamped_row + viewport_height).min(total_rows);
viewport_changed = true;
} else if clamped_row >= self.viewport_rows.end {
let new_start = clamped_row.saturating_sub(viewport_height.saturating_sub(1));
self.viewport_rows = new_start..(new_start + viewport_height).min(total_rows);
viewport_changed = true;
}
if viewport_changed {
debug!(target: "viewport_manager",
"Crosshair row set to: {}, adjusted viewport to: {:?}",
clamped_row, self.viewport_rows);
} else {
debug!(target: "viewport_manager",
"Crosshair row set to: {}", clamped_row);
}
}
pub fn set_crosshair_column(&mut self, col: usize) {
let total_columns = self.dataview.get_display_columns().len();
let clamped_col = col.min(total_columns.saturating_sub(1));
self.crosshair_col = clamped_col;
if self.viewport_lock {
debug!(target: "viewport_manager",
"Crosshair column set to: {} (viewport locked, no scroll adjustment)",
clamped_col);
return;
}
let _terminal_width = self.terminal_width.saturating_sub(4); if self.set_current_column(clamped_col) {
debug!(target: "viewport_manager",
"Crosshair column set to: {} with viewport adjustment", clamped_col);
} else {
debug!(target: "viewport_manager",
"Crosshair column set to: {}", clamped_col);
}
}
#[must_use]
pub fn get_crosshair_col(&self) -> usize {
self.crosshair_col
}
#[must_use]
pub fn get_crosshair_row(&self) -> usize {
self.crosshair_row
}
#[must_use]
pub fn get_selected_row(&self) -> usize {
self.crosshair_row
}
#[must_use]
pub fn get_selected_column(&self) -> usize {
self.crosshair_col
}
#[must_use]
pub fn get_crosshair_position(&self) -> (usize, usize) {
(self.crosshair_row, self.crosshair_col)
}
#[must_use]
pub fn get_scroll_offset(&self) -> (usize, usize) {
(self.viewport_rows.start, self.viewport_cols.start)
}
pub fn set_scroll_offset(&mut self, row_offset: usize, col_offset: usize) {
let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
let viewport_width = self.viewport_cols.end - self.viewport_cols.start;
self.viewport_rows = row_offset..(row_offset + viewport_height);
self.viewport_cols = col_offset..(col_offset + viewport_width);
if self.crosshair_row < self.viewport_rows.start {
self.crosshair_row = self.viewport_rows.start;
} else if self.crosshair_row >= self.viewport_rows.end {
self.crosshair_row = self.viewport_rows.end.saturating_sub(1);
}
if self.crosshair_col < self.viewport_cols.start {
self.crosshair_col = self.viewport_cols.start;
} else if self.crosshair_col >= self.viewport_cols.end {
self.crosshair_col = self.viewport_cols.end.saturating_sub(1);
}
self.cache_dirty = true;
}
#[must_use]
pub fn get_crosshair_viewport_position(&self) -> Option<(usize, usize)> {
if self.crosshair_row < self.viewport_rows.start
|| self.crosshair_row >= self.viewport_rows.end
{
return None;
}
let pinned_count = self.dataview.get_pinned_columns().len();
if self.crosshair_col < pinned_count {
return Some((
self.crosshair_row - self.viewport_rows.start,
self.crosshair_col, ));
}
let scrollable_col = self.crosshair_col - pinned_count;
if scrollable_col >= self.viewport_cols.start && scrollable_col < self.viewport_cols.end {
let visual_col_in_viewport = pinned_count + (scrollable_col - self.viewport_cols.start);
return Some((
self.crosshair_row - self.viewport_rows.start,
visual_col_in_viewport,
));
}
None
}
pub fn navigate_row_up(&mut self) -> RowNavigationResult {
let total_rows = self.dataview.row_count();
if self.viewport_lock {
debug!(target: "viewport_manager",
"navigate_row_up: Viewport locked, crosshair={}, viewport={:?}",
self.crosshair_row, self.viewport_rows);
if self.crosshair_row > self.viewport_rows.start {
self.crosshair_row -= 1;
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description: "Moved within locked viewport".to_string(),
viewport_changed: false,
};
}
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description: "Moved within locked viewport".to_string(),
viewport_changed: false,
};
}
if self.cursor_lock {
if let Some(lock_position) = self.cursor_lock_position {
if self.viewport_rows.start == 0 {
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description: "At top of data".to_string(),
viewport_changed: false,
};
}
let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
let new_viewport_start = self.viewport_rows.start.saturating_sub(1);
self.viewport_rows =
new_viewport_start..(new_viewport_start + viewport_height).min(total_rows);
self.crosshair_row = (self.viewport_rows.start + lock_position)
.min(self.viewport_rows.end.saturating_sub(1));
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description: format!(
"Scrolled up (locked at viewport row {})",
lock_position + 1
),
viewport_changed: true,
};
}
}
if self.crosshair_row == 0 {
return RowNavigationResult {
row_position: 0,
row_scroll_offset: self.viewport_rows.start,
description: "Already at first row".to_string(),
viewport_changed: false,
};
}
let new_row = self.crosshair_row - 1;
self.crosshair_row = new_row;
let viewport_changed = if new_row < self.viewport_rows.start {
self.viewport_rows = new_row..self.viewport_rows.end.saturating_sub(1);
true
} else {
false
};
RowNavigationResult {
row_position: new_row,
row_scroll_offset: self.viewport_rows.start,
description: format!("Move to row {}", new_row + 1),
viewport_changed,
}
}
pub fn navigate_row_down(&mut self) -> RowNavigationResult {
let total_rows = self.dataview.row_count();
if self.viewport_lock {
debug!(target: "viewport_manager",
"navigate_row_down: Viewport locked, crosshair={}, viewport={:?}",
self.crosshair_row, self.viewport_rows);
if self.crosshair_row < self.viewport_rows.end - 1
&& self.crosshair_row < total_rows - 1
{
self.crosshair_row += 1;
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description: "Moved within locked viewport".to_string(),
viewport_changed: false,
};
}
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description: "Moved within locked viewport".to_string(),
viewport_changed: false,
};
}
if self.cursor_lock {
if let Some(lock_position) = self.cursor_lock_position {
let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
let new_viewport_start =
(self.viewport_rows.start + 1).min(total_rows.saturating_sub(viewport_height));
if new_viewport_start == self.viewport_rows.start {
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description: "At bottom of data".to_string(),
viewport_changed: false,
};
}
self.viewport_rows =
new_viewport_start..(new_viewport_start + viewport_height).min(total_rows);
self.crosshair_row = (self.viewport_rows.start + lock_position)
.min(self.viewport_rows.end.saturating_sub(1));
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description: format!(
"Scrolled down (locked at viewport row {})",
lock_position + 1
),
viewport_changed: true,
};
}
}
if self.crosshair_row + 1 >= total_rows {
let last_row = total_rows.saturating_sub(1);
return RowNavigationResult {
row_position: last_row,
row_scroll_offset: self.viewport_rows.start,
description: "Already at last row".to_string(),
viewport_changed: false,
};
}
let new_row = self.crosshair_row + 1;
self.crosshair_row = new_row;
let viewport_changed = if new_row >= self.viewport_rows.end {
let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
self.viewport_rows = (new_row + 1).saturating_sub(viewport_height)..(new_row + 1);
true
} else {
false
};
RowNavigationResult {
row_position: new_row,
row_scroll_offset: self.viewport_rows.start,
description: format!("Move to row {}", new_row + 1),
viewport_changed,
}
}
#[must_use]
pub fn new(dataview: Arc<DataView>) -> Self {
let display_columns = dataview.get_display_columns();
let visible_col_count = display_columns.len();
let _total_col_count = dataview.source().column_count(); let total_rows = dataview.row_count();
let initial_viewport_cols = if visible_col_count > 0 {
0..visible_col_count.min(20) } else {
0..0
};
let default_visible_rows = 50usize; let initial_viewport_rows = if total_rows > 0 {
0..total_rows.min(default_visible_rows)
} else {
0..0
};
Self {
dataview,
viewport_rows: initial_viewport_rows,
viewport_cols: initial_viewport_cols,
terminal_width: 80,
terminal_height: 24,
width_calculator: ColumnWidthCalculator::new(),
visible_row_cache: Vec::new(),
cache_signature: 0,
cache_dirty: true,
crosshair_row: 0,
crosshair_col: 0,
cursor_lock: false,
cursor_lock_position: None,
viewport_lock: false,
viewport_lock_boundaries: None,
}
}
pub fn set_dataview(&mut self, dataview: Arc<DataView>) {
self.dataview = dataview;
self.invalidate_cache();
}
pub fn reset_crosshair(&mut self) {
self.crosshair_row = 0;
self.crosshair_col = 0;
self.cursor_lock = false;
self.cursor_lock_position = None;
}
#[must_use]
pub fn get_packing_mode(&self) -> ColumnPackingMode {
self.width_calculator.get_packing_mode()
}
pub fn set_packing_mode(&mut self, mode: ColumnPackingMode) {
self.width_calculator.set_packing_mode(mode);
self.invalidate_cache();
}
pub fn cycle_packing_mode(&mut self) -> ColumnPackingMode {
self.width_calculator.cycle_packing_mode();
self.invalidate_cache();
self.width_calculator.get_packing_mode()
}
pub fn set_viewport(&mut self, row_offset: usize, col_offset: usize, width: u16, height: u16) {
let new_rows = row_offset
..row_offset
.saturating_add(height as usize)
.min(self.dataview.row_count());
let display_columns = self.dataview.get_display_columns();
let visual_column_count = display_columns.len();
let columns_that_fit = self.calculate_columns_that_fit(col_offset, width);
let new_cols = col_offset
..col_offset
.saturating_add(columns_that_fit)
.min(visual_column_count);
if new_rows != self.viewport_rows || new_cols != self.viewport_cols {
self.viewport_rows = new_rows;
self.viewport_cols = new_cols;
self.terminal_width = width;
self.terminal_height = height;
self.cache_dirty = true;
}
}
pub fn update_terminal_size(&mut self, terminal_width: u16, terminal_height: u16) -> usize {
let visible_rows = (terminal_height as usize).max(10);
debug!(target: "viewport_manager",
"update_terminal_size: terminal_height={}, calculated visible_rows={}",
terminal_height, visible_rows
);
let old_viewport = self.viewport_rows.clone();
self.terminal_width = terminal_width;
self.terminal_height = terminal_height;
let total_rows = self.dataview.row_count();
let viewport_size = self.viewport_rows.end - self.viewport_rows.start;
if viewport_size != visible_rows && total_rows > 0 {
if self.crosshair_row < self.viewport_rows.start {
self.viewport_rows =
self.crosshair_row..(self.crosshair_row + visible_rows).min(total_rows);
} else if self.crosshair_row >= self.viewport_rows.start + visible_rows {
let start = self.crosshair_row.saturating_sub(visible_rows - 1);
self.viewport_rows = start..(start + visible_rows).min(total_rows);
} else {
self.viewport_rows = self.viewport_rows.start
..(self.viewport_rows.start + visible_rows).min(total_rows);
}
}
let visible_column_count = self.dataview.get_display_columns().len();
if visible_column_count > 0 {
let columns_that_fit = self.calculate_columns_that_fit(
self.viewport_cols.start,
terminal_width.saturating_sub(2), );
let new_col_viewport_end = self
.viewport_cols
.start
.saturating_add(columns_that_fit)
.min(visible_column_count);
let old_col_viewport = self.viewport_cols.clone();
self.viewport_cols = self.viewport_cols.start..new_col_viewport_end;
if old_col_viewport != self.viewport_cols {
debug!(target: "viewport_manager",
"update_terminal_size - column viewport changed from {:?} to {:?}, terminal_width={}",
old_col_viewport, self.viewport_cols, terminal_width
);
self.cache_dirty = true;
}
}
if old_viewport != self.viewport_rows {
debug!(target: "navigation",
"ViewportManager::update_terminal_size - viewport changed from {:?} to {:?}, crosshair={}, visible_rows={}",
old_viewport, self.viewport_rows, self.crosshair_row, visible_rows
);
}
visible_rows
}
pub fn scroll_by(&mut self, row_delta: isize, col_delta: isize) {
let new_row_start = (self.viewport_rows.start as isize + row_delta).max(0) as usize;
let new_col_start = (self.viewport_cols.start as isize + col_delta).max(0) as usize;
self.set_viewport(
new_row_start,
new_col_start,
self.terminal_width,
self.terminal_height,
);
}
pub fn get_column_widths(&mut self) -> &[u16] {
self.width_calculator
.get_all_column_widths(&self.dataview, &self.viewport_rows)
}
pub fn get_column_width(&mut self, col_idx: usize) -> u16 {
self.width_calculator
.get_column_width(&self.dataview, &self.viewport_rows, col_idx)
}
#[must_use]
pub fn get_visible_rows(&self) -> Vec<DataRow> {
let mut rows = Vec::with_capacity(self.viewport_rows.len());
for row_idx in self.viewport_rows.clone() {
if let Some(row) = self.dataview.get_row(row_idx) {
rows.push(row);
}
}
rows
}
#[must_use]
pub fn get_visible_row(&self, viewport_row: usize) -> Option<DataRow> {
let absolute_row = self.viewport_rows.start + viewport_row;
if absolute_row < self.viewport_rows.end {
self.dataview.get_row(absolute_row)
} else {
None
}
}
#[must_use]
pub fn get_visible_columns(&self) -> Vec<String> {
let display_column_names = self.dataview.get_display_column_names();
let mut visible = Vec::new();
for col_idx in self.viewport_cols.clone() {
if col_idx < display_column_names.len() {
visible.push(display_column_names[col_idx].clone());
}
}
visible
}
#[must_use]
pub fn viewport_rows(&self) -> Range<usize> {
self.viewport_rows.clone()
}
#[must_use]
pub fn viewport_cols(&self) -> Range<usize> {
self.viewport_cols.clone()
}
#[must_use]
pub fn is_row_visible(&self, row_idx: usize) -> bool {
self.viewport_rows.contains(&row_idx)
}
#[must_use]
pub fn is_column_visible(&self, col_idx: usize) -> bool {
self.viewport_cols.contains(&col_idx)
}
#[must_use]
pub fn total_rows(&self) -> usize {
self.dataview.row_count()
}
#[must_use]
pub fn total_columns(&self) -> usize {
self.dataview.column_count()
}
#[must_use]
pub fn get_terminal_width(&self) -> u16 {
self.terminal_width
}
#[must_use]
pub fn get_terminal_height(&self) -> usize {
self.terminal_height as usize
}
pub fn invalidate_cache(&mut self) {
self.cache_dirty = true;
self.width_calculator.mark_dirty();
}
pub fn calculate_visible_column_indices(&mut self, available_width: u16) -> Vec<usize> {
let display_columns = self.dataview.get_display_columns();
let total_visual_columns = display_columns.len();
if total_visual_columns == 0 {
return Vec::new();
}
let pinned_columns = self.dataview.get_pinned_columns();
let pinned_count = pinned_columns.len();
let mut used_width = 0u16;
let separator_width = 1u16;
let mut result = Vec::new();
tracing::debug!("[PIN_DEBUG] === calculate_visible_column_indices ===");
tracing::debug!(
"[PIN_DEBUG] available_width={}, total_visual_columns={}",
available_width,
total_visual_columns
);
tracing::debug!(
"[PIN_DEBUG] pinned_columns={:?} (count={})",
pinned_columns,
pinned_count
);
tracing::debug!("[PIN_DEBUG] viewport_cols={:?}", self.viewport_cols);
tracing::debug!("[PIN_DEBUG] display_columns={:?}", display_columns);
debug!(target: "viewport_manager",
"calculate_visible_column_indices: available_width={}, total_visual_columns={}, pinned_count={}, viewport_start={}",
available_width, total_visual_columns, pinned_count, self.viewport_cols.start);
for visual_idx in 0..pinned_count {
if visual_idx >= display_columns.len() {
break;
}
let datatable_idx = display_columns[visual_idx];
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
datatable_idx,
);
used_width += width + separator_width;
result.push(datatable_idx);
tracing::debug!(
"[PIN_DEBUG] Added pinned column: visual_idx={}, datatable_idx={}, width={}",
visual_idx,
datatable_idx,
width
);
}
let scrollable_start = self.viewport_cols.start;
let visual_start = scrollable_start + pinned_count;
tracing::debug!(
"[PIN_DEBUG] viewport_cols.start={} is SCROLLABLE index",
self.viewport_cols.start
);
tracing::debug!(
"[PIN_DEBUG] visual_start={} (scrollable_start {} + pinned_count {})",
visual_start,
scrollable_start,
pinned_count
);
let visual_start = visual_start.min(total_visual_columns);
for visual_idx in visual_start..total_visual_columns {
let datatable_idx = display_columns[visual_idx];
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
datatable_idx,
);
if used_width + width + separator_width <= available_width {
used_width += width + separator_width;
result.push(datatable_idx);
tracing::debug!("[PIN_DEBUG] Added scrollable column: visual_idx={}, datatable_idx={}, width={}", visual_idx, datatable_idx, width);
} else {
tracing::debug!(
"[PIN_DEBUG] Stopped at visual_idx={} - would exceed width",
visual_idx
);
break;
}
}
if result.is_empty() && total_visual_columns > 0 {
result.push(display_columns[0]);
}
tracing::debug!("[PIN_DEBUG] Final result: {:?}", result);
tracing::debug!("[PIN_DEBUG] === End calculate_visible_column_indices ===");
debug!(target: "viewport_manager",
"calculate_visible_column_indices RESULT: pinned={}, viewport_start={}, visual_start={} -> DataTable indices {:?}",
pinned_count, self.viewport_cols.start, visual_start, result);
result
}
pub fn calculate_columns_that_fit(&mut self, start_col: usize, available_width: u16) -> usize {
let mut used_width = 0u16;
let mut column_count = 0usize;
let separator_width = 1u16;
for col_idx in start_col..self.dataview.column_count() {
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
if used_width + width + separator_width <= available_width {
used_width += width + separator_width;
column_count += 1;
} else {
break;
}
}
column_count.max(1) }
pub fn get_column_widths_for(&mut self, column_indices: &[usize]) -> Vec<u16> {
column_indices
.iter()
.map(|&idx| {
self.width_calculator
.get_column_width(&self.dataview, &self.viewport_rows, idx)
})
.collect()
}
pub fn update_column_viewport(&mut self, start_col: usize, available_width: u16) {
let col_count = self.calculate_columns_that_fit(start_col, available_width);
let end_col = (start_col + col_count).min(self.dataview.column_count());
if self.viewport_cols.start != start_col || self.viewport_cols.end != end_col {
self.viewport_cols = start_col..end_col;
self.cache_dirty = true;
}
}
#[must_use]
pub fn dataview(&self) -> &DataView {
&self.dataview
}
#[must_use]
pub fn clone_dataview(&self) -> DataView {
(*self.dataview).clone()
}
pub fn calculate_optimal_offset_for_last_column(&mut self, available_width: u16) -> usize {
let display_columns = self.dataview.get_display_columns();
if display_columns.is_empty() {
return 0;
}
let pinned = self.dataview.get_pinned_columns();
let _pinned_count = pinned.len();
let mut pinned_width = 0u16;
let separator_width = 1u16;
for &col_idx in pinned {
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
pinned_width += width + separator_width;
}
let available_for_scrollable = available_width.saturating_sub(pinned_width);
let scrollable_columns: Vec<usize> = display_columns
.iter()
.filter(|&&col| !pinned.contains(&col))
.copied()
.collect();
if scrollable_columns.is_empty() {
return 0;
}
let last_col_idx = *scrollable_columns.last().unwrap();
let last_col_width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
last_col_idx,
);
tracing::debug!(
"Starting calculation: last_col_idx={}, width={}w, available={}w, scrollable_cols={}",
last_col_idx,
last_col_width,
available_for_scrollable,
scrollable_columns.len()
);
let mut accumulated_width = last_col_width + separator_width;
let mut best_offset = scrollable_columns.len() - 1;
for (idx, &col_idx) in scrollable_columns.iter().enumerate().rev().skip(1) {
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
let width_with_separator = width + separator_width;
if accumulated_width + width_with_separator <= available_for_scrollable {
accumulated_width += width_with_separator;
best_offset = idx; tracing::trace!(
"Column {} (idx {}) fits ({}w), accumulated={}w, new offset={}",
col_idx,
idx,
width,
accumulated_width,
best_offset
);
} else {
best_offset = idx + 1;
tracing::trace!(
"Column {} doesn't fit ({}w would make {}w total), stopping at offset {}",
col_idx,
width,
accumulated_width + width_with_separator,
best_offset
);
break;
}
}
let mut test_width = 0u16;
let mut can_see_last = false;
for idx in best_offset..scrollable_columns.len() {
let col_idx = scrollable_columns[idx];
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
test_width += width + separator_width;
if test_width > available_for_scrollable {
tracing::warn!(
"Offset {} doesn't show last column! Need {}w but have {}w",
best_offset,
test_width,
available_for_scrollable
);
best_offset += 1;
can_see_last = false;
break;
}
if idx == scrollable_columns.len() - 1 {
can_see_last = true;
}
}
while !can_see_last && best_offset < scrollable_columns.len() {
test_width = 0;
for idx in best_offset..scrollable_columns.len() {
let col_idx = scrollable_columns[idx];
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
test_width += width + separator_width;
if test_width > available_for_scrollable {
best_offset += 1;
break;
}
if idx == scrollable_columns.len() - 1 {
can_see_last = true;
}
}
}
tracing::debug!(
"Final offset for last column: scrollable_offset={}, fits {} columns, last col width: {}w, verified last col visible: {}",
best_offset,
scrollable_columns.len() - best_offset,
last_col_width,
can_see_last
);
best_offset
}
pub fn debug_dump(&mut self, available_width: u16) -> String {
let mut output = String::new();
output.push_str("========== VIEWPORT MANAGER DEBUG ==========\n");
let total_cols = self.dataview.column_count();
let pinned = self.dataview.get_pinned_columns();
let pinned_count = pinned.len();
output.push_str(&format!("Total columns: {total_cols}\n"));
output.push_str(&format!("Pinned columns: {pinned:?}\n"));
output.push_str(&format!("Available width: {available_width}w\n"));
output.push_str(&format!("Current viewport: {:?}\n", self.viewport_cols));
output.push_str(&format!(
"Packing mode: {} (Alt+S to cycle)\n",
self.width_calculator.get_packing_mode().display_name()
));
output.push('\n');
output.push_str("=== COLUMN WIDTH CALCULATIONS ===\n");
output.push_str(&format!(
"Mode: {}\n",
self.width_calculator.get_packing_mode().display_name()
));
let debug_info = self.width_calculator.get_debug_info();
if !debug_info.is_empty() {
output.push_str("Visible columns in viewport:\n");
let mut visible_count = 0;
for col_idx in self.viewport_cols.clone() {
if col_idx < debug_info.len() {
let (ref col_name, header_width, max_data_width, final_width, sample_count) =
debug_info[col_idx];
let reason = match self.width_calculator.get_packing_mode() {
ColumnPackingMode::DataFocus => {
if max_data_width <= 3 {
format!("Ultra aggressive (data:{max_data_width}≤3 chars)")
} else if max_data_width <= 10 && header_width > max_data_width * 2 {
format!(
"Aggressive truncate (data:{}≤10, header:{}>{} )",
max_data_width,
header_width,
max_data_width * 2
)
} else if final_width == MAX_COL_WIDTH_DATA_FOCUS {
"Max width reached".to_string()
} else {
"Data-based width".to_string()
}
}
ColumnPackingMode::HeaderFocus => {
if final_width == header_width + COLUMN_PADDING {
"Full header shown".to_string()
} else if final_width == MAX_COL_WIDTH {
"Max width reached".to_string()
} else {
"Header priority".to_string()
}
}
ColumnPackingMode::Balanced => {
if header_width > max_data_width && final_width < header_width {
"Header constrained by ratio".to_string()
} else {
"Balanced".to_string()
}
}
};
output.push_str(&format!(
" [{col_idx}] \"{col_name}\":\n Header: {header_width}w, Data: {max_data_width}w → Final: {final_width}w ({reason}, {sample_count} samples)\n"
));
visible_count += 1;
if visible_count >= 10 {
let remaining = self.viewport_cols.end - self.viewport_cols.start - 10;
if remaining > 0 {
output.push_str(&format!(" ... and {remaining} more columns\n"));
}
break;
}
}
}
}
output.push('\n');
output.push_str("Column width summary (all columns):\n");
let all_widths = self
.width_calculator
.get_all_column_widths(&self.dataview, &self.viewport_rows);
for (idx, &width) in all_widths.iter().enumerate() {
if idx >= 20 && idx < total_cols - 10 {
if idx == 20 {
output.push_str(" ... (showing only first 20 and last 10)\n");
}
continue;
}
output.push_str(&format!(" [{idx}] {width}w\n"));
}
output.push('\n');
output.push_str("=== OPTIMAL OFFSET CALCULATION ===\n");
let last_col_idx = total_cols - 1;
let last_col_width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
last_col_idx,
);
let separator_width = 1u16;
let mut pinned_width = 0u16;
for &col_idx in pinned {
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
pinned_width += width + separator_width;
}
let available_for_scrollable = available_width.saturating_sub(pinned_width);
output.push_str(&format!(
"Last column: {last_col_idx} (width: {last_col_width}w)\n"
));
output.push_str(&format!("Pinned width: {pinned_width}w\n"));
output.push_str(&format!(
"Available for scrollable: {available_for_scrollable}w\n"
));
output.push('\n');
let mut accumulated_width = last_col_width + separator_width;
let mut best_offset = last_col_idx;
output.push_str("Backtracking from last column:\n");
output.push_str(&format!(
" Start: column {last_col_idx} = {last_col_width}w (accumulated: {accumulated_width}w)\n"
));
for col_idx in (pinned_count..last_col_idx).rev() {
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
let width_with_sep = width + separator_width;
if accumulated_width + width_with_sep <= available_for_scrollable {
accumulated_width += width_with_sep;
best_offset = col_idx;
output.push_str(&format!(
" Column {col_idx} fits: {width}w (accumulated: {accumulated_width}w, offset: {best_offset})\n"
));
} else {
output.push_str(&format!(
" Column {} doesn't fit: {}w (would make {}w > {}w)\n",
col_idx,
width,
accumulated_width + width_with_sep,
available_for_scrollable
));
best_offset = col_idx + 1;
break;
}
}
output.push_str(&format!("\nCalculated offset: {best_offset} (absolute)\n"));
output.push_str("\n=== VERIFICATION ===\n");
let mut verify_width = 0u16;
let mut can_show_last = true;
for test_idx in best_offset..=last_col_idx {
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
test_idx,
);
verify_width += width + separator_width;
output.push_str(&format!(
" Column {test_idx}: {width}w (running total: {verify_width}w)\n"
));
if verify_width > available_for_scrollable {
output.push_str(&format!(
" ❌ EXCEEDS LIMIT! {verify_width}w > {available_for_scrollable}w\n"
));
if test_idx == last_col_idx {
can_show_last = false;
output.push_str(" ❌ LAST COLUMN NOT VISIBLE!\n");
}
break;
}
if test_idx == last_col_idx {
output.push_str(" ✅ LAST COLUMN VISIBLE!\n");
}
}
output.push_str(&format!(
"\nVerification result: last column visible = {can_show_last}\n"
));
output.push_str("\n=== CURRENT VIEWPORT RESULT ===\n");
let visible_indices = self.calculate_visible_column_indices(available_width);
output.push_str(&format!("Visible columns: {visible_indices:?}\n"));
output.push_str(&format!(
"Last visible column: {}\n",
visible_indices.last().copied().unwrap_or(0)
));
output.push_str(&format!(
"Shows last column ({}): {}\n",
last_col_idx,
visible_indices.contains(&last_col_idx)
));
output.push_str("============================================\n");
output
}
#[must_use]
pub fn get_column_names_ordered(&self) -> Vec<String> {
self.dataview.column_names()
}
pub fn get_visible_columns_info(
&mut self,
available_width: u16,
) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
debug!(target: "viewport_manager",
"get_visible_columns_info CALLED with width={}, current_viewport={:?}",
available_width, self.viewport_cols);
let viewport_indices = self.calculate_visible_column_indices(available_width);
let display_order = self.dataview.get_display_columns();
let mut visible_indices = Vec::new();
for &col_idx in &display_order {
if viewport_indices.contains(&col_idx) {
visible_indices.push(col_idx);
}
}
let pinned_columns = self.dataview.get_pinned_columns();
let mut pinned_visible = Vec::new();
let mut scrollable_visible = Vec::new();
for &idx in &visible_indices {
if pinned_columns.contains(&idx) {
pinned_visible.push(idx);
} else {
scrollable_visible.push(idx);
}
}
debug!(target: "viewport_manager",
"get_visible_columns_info: viewport={:?} -> ordered={:?} ({} pinned, {} scrollable)",
viewport_indices, visible_indices, pinned_visible.len(), scrollable_visible.len());
debug!(target: "viewport_manager",
"RENDERER DEBUG: viewport_indices={:?}, display_order={:?}, visible_indices={:?}",
viewport_indices, display_order, visible_indices);
(visible_indices, pinned_visible, scrollable_visible)
}
pub fn calculate_column_x_positions(&mut self, available_width: u16) -> (Vec<usize>, Vec<u16>) {
let visible_indices = self.calculate_visible_column_indices(available_width);
let mut x_positions = Vec::new();
let mut current_x = 0u16;
let separator_width = 1u16;
for &col_idx in &visible_indices {
x_positions.push(current_x);
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
current_x += width + separator_width;
}
(visible_indices, x_positions)
}
pub fn get_column_x_position(&mut self, column: usize, available_width: u16) -> Option<u16> {
let (indices, positions) = self.calculate_column_x_positions(available_width);
indices
.iter()
.position(|&idx| idx == column)
.and_then(|pos| positions.get(pos).copied())
}
pub fn calculate_visible_column_indices_ordered(&mut self, available_width: u16) -> Vec<usize> {
let ordered_columns = self.dataview.get_display_columns();
let mut visible_indices = Vec::new();
let mut used_width = 0u16;
let separator_width = 1u16;
tracing::trace!(
"ViewportManager: Starting ordered column layout. Available width: {}w, DataView order: {:?}",
available_width,
ordered_columns
);
for &col_idx in &ordered_columns {
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
if used_width + width + separator_width <= available_width {
visible_indices.push(col_idx);
used_width += width + separator_width;
tracing::trace!(
"Added column {} in DataView order: {}w (total used: {}w)",
col_idx,
width,
used_width
);
} else {
tracing::trace!(
"Skipped column {} ({}w) - would exceed available width",
col_idx,
width
);
break; }
}
tracing::trace!(
"Final ordered layout: {} columns visible {:?}, {}w used of {}w",
visible_indices.len(),
visible_indices,
used_width,
available_width
);
visible_indices
}
pub fn get_display_position_for_datatable_column(
&mut self,
datatable_column: usize,
available_width: u16,
) -> Option<usize> {
let visible_columns_info = self.get_visible_columns_info(available_width);
let visible_indices = visible_columns_info.0;
let position = visible_indices
.iter()
.position(|&col| col == datatable_column);
debug!(target: "viewport_manager",
"get_display_position_for_datatable_column: datatable_column={}, visible_indices={:?}, position={:?}",
datatable_column, visible_indices, position);
position
}
pub fn get_crosshair_column(
&mut self,
current_datatable_column: usize,
available_width: u16,
) -> Option<usize> {
let visible_columns_info = self.get_visible_columns_info(available_width);
let visible_indices = visible_columns_info.0;
let position = visible_indices
.iter()
.position(|&col| col == current_datatable_column);
debug!(target: "viewport_manager",
"CROSSHAIR: current_datatable_column={}, visible_indices={:?}, crosshair_position={:?}",
current_datatable_column, visible_indices, position);
position
}
pub fn get_visual_display(
&mut self,
available_width: u16,
_row_indices: &[usize], ) -> (Vec<String>, Vec<Vec<String>>, Vec<u16>) {
let row_indices: Vec<usize> = (self.viewport_rows.start..self.viewport_rows.end).collect();
debug!(target: "viewport_manager",
"get_visual_display: Using viewport_rows {:?} -> row_indices: {:?} (first 5)",
self.viewport_rows,
row_indices.iter().take(5).collect::<Vec<_>>());
let visible_column_indices = self.calculate_visible_column_indices(available_width);
tracing::debug!(
"[RENDER_DEBUG] visible_column_indices from calculate: {:?}",
visible_column_indices
);
let all_headers = self.dataview.get_display_column_names();
let display_columns = self.dataview.get_display_columns();
let total_visual_columns = all_headers.len();
debug!(target: "viewport_manager",
"get_visual_display: {} total visual columns, viewport: {:?}",
total_visual_columns, self.viewport_cols);
let headers: Vec<String> = visible_column_indices
.iter()
.filter_map(|&dt_idx| {
display_columns
.iter()
.position(|&x| x == dt_idx)
.and_then(|visual_idx| all_headers.get(visual_idx).cloned())
})
.collect();
tracing::debug!("[RENDER_DEBUG] headers: {:?}", headers);
let visual_rows: Vec<Vec<String>> = row_indices
.iter()
.filter_map(|&display_row_idx| {
let row_data = self.dataview.get_row_visual_values(display_row_idx);
if let Some(ref full_row) = row_data {
if !(5..19900).contains(&display_row_idx) {
debug!(target: "viewport_manager",
"DATAVIEW FETCH: display_row_idx {} -> data: {:?} (first 3 cols)",
display_row_idx,
full_row.iter().take(3).collect::<Vec<_>>());
}
}
row_data.map(|full_row| {
visible_column_indices
.iter()
.filter_map(|&dt_idx| {
display_columns
.iter()
.position(|&x| x == dt_idx)
.and_then(|visual_idx| full_row.get(visual_idx).cloned())
})
.collect()
})
})
.collect();
let widths: Vec<u16> = visible_column_indices
.iter()
.map(|&dt_idx| {
self.width_calculator
.get_column_width(&self.dataview, &self.viewport_rows, dt_idx)
})
.collect();
debug!(target: "viewport_manager",
"get_visual_display RESULT: {} headers, {} rows",
headers.len(), visual_rows.len());
if let Some(first_row) = visual_rows.first() {
debug!(target: "viewport_manager",
"Alignment check (FIRST ROW): {:?}",
headers.iter().zip(first_row).take(5)
.map(|(h, v)| format!("{h}: {v}")).collect::<Vec<_>>());
}
if let Some(last_row) = visual_rows.last() {
debug!(target: "viewport_manager",
"Alignment check (LAST ROW): {:?}",
headers.iter().zip(last_row).take(5)
.map(|(h, v)| format!("{h}: {v}")).collect::<Vec<_>>());
}
(headers, visual_rows, widths)
}
pub fn get_visible_column_headers(&self, visible_indices: &[usize]) -> Vec<String> {
let mut headers = Vec::new();
let source = self.dataview.source();
let all_column_names = source.column_names();
for &visual_idx in visible_indices {
if visual_idx < all_column_names.len() {
headers.push(all_column_names[visual_idx].clone());
} else {
headers.push(format!("Column_{visual_idx}"));
}
}
debug!(target: "viewport_manager",
"get_visible_column_headers: indices={:?} -> headers={:?}",
visible_indices, headers);
headers
}
pub fn get_crosshair_column_for_display(
&mut self,
current_display_position: usize,
available_width: u16,
) -> Option<usize> {
let display_columns = self.dataview.get_display_columns();
if current_display_position >= display_columns.len() {
debug!(target: "viewport_manager",
"CROSSHAIR DISPLAY: display_position {} out of bounds (max {})",
current_display_position, display_columns.len());
return None;
}
let datatable_column = display_columns[current_display_position];
let visible_columns_info = self.get_visible_columns_info(available_width);
let visible_indices = visible_columns_info.0;
let position = visible_indices
.iter()
.position(|&col| col == datatable_column);
debug!(target: "viewport_manager",
"CROSSHAIR DISPLAY: display_pos={} -> datatable_col={} -> visible_indices={:?} -> crosshair_pos={:?}",
current_display_position, datatable_column, visible_indices, position);
position
}
pub fn calculate_efficiency_metrics(&mut self, available_width: u16) -> ViewportEfficiency {
let visible_indices = self.calculate_visible_column_indices(available_width);
let mut used_width = 0u16;
let separator_width = 1u16;
for &col_idx in &visible_indices {
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
used_width += width + separator_width;
}
if !visible_indices.is_empty() {
used_width = used_width.saturating_sub(separator_width);
}
let wasted_space = available_width.saturating_sub(used_width);
let next_column_width = if visible_indices.is_empty() {
None
} else {
let last_visible = *visible_indices.last().unwrap();
if last_visible + 1 < self.dataview.column_count() {
Some(self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
last_visible + 1,
))
} else {
None
}
};
let mut columns_that_could_fit = Vec::new();
if wasted_space > MIN_COL_WIDTH + separator_width {
let all_widths = self
.width_calculator
.get_all_column_widths(&self.dataview, &self.viewport_rows);
for (idx, &width) in all_widths.iter().enumerate() {
if !visible_indices.contains(&idx) && width + separator_width <= wasted_space {
columns_that_could_fit.push((idx, width));
}
}
}
let efficiency_percent = if available_width > 0 {
((f32::from(used_width) / f32::from(available_width)) * 100.0) as u8
} else {
0
};
ViewportEfficiency {
available_width,
used_width,
wasted_space,
efficiency_percent,
visible_columns: visible_indices.len(),
column_widths: visible_indices
.iter()
.map(|&idx| {
self.width_calculator
.get_column_width(&self.dataview, &self.viewport_rows, idx)
})
.collect(),
next_column_width,
columns_that_could_fit,
}
}
pub fn navigate_to_first_column(&mut self) -> NavigationResult {
if self.viewport_lock {
self.crosshair_col = self.viewport_cols.start;
return NavigationResult {
column_position: self.crosshair_col,
scroll_offset: self.viewport_cols.start,
description: "Moved to first visible column (viewport locked)".to_string(),
viewport_changed: false,
};
}
let pinned_count = self.dataview.get_pinned_columns().len();
let pinned_names = self.dataview.get_pinned_column_names();
let first_scrollable_column = pinned_count;
let new_scroll_offset = 0;
let old_scroll_offset = self.viewport_cols.start;
let visible_indices = self
.calculate_visible_column_indices_with_offset(self.terminal_width, new_scroll_offset);
let viewport_end = if let Some(&last_idx) = visible_indices.last() {
last_idx + 1
} else {
new_scroll_offset + 1
};
self.viewport_cols = new_scroll_offset..viewport_end;
self.crosshair_col = first_scrollable_column;
let description = if pinned_count > 0 {
format!(
"First scrollable column selected (after {pinned_count} pinned: {pinned_names:?})"
)
} else {
"First column selected".to_string()
};
let viewport_changed = old_scroll_offset != new_scroll_offset;
debug!(target: "viewport_manager",
"navigate_to_first_column: pinned={}, first_scrollable={}, crosshair_col={}, scroll_offset={}->{}",
pinned_count, first_scrollable_column, self.crosshair_col, old_scroll_offset, new_scroll_offset);
NavigationResult {
column_position: first_scrollable_column,
scroll_offset: new_scroll_offset,
description,
viewport_changed,
}
}
pub fn navigate_to_last_column(&mut self) -> NavigationResult {
if self.viewport_lock {
self.crosshair_col = self.viewport_cols.end.saturating_sub(1);
return NavigationResult {
column_position: self.crosshair_col,
scroll_offset: self.viewport_cols.start,
description: "Moved to last visible column (viewport locked)".to_string(),
viewport_changed: false,
};
}
let display_columns = self.dataview.get_display_columns();
let total_visual_columns = display_columns.len();
if total_visual_columns == 0 {
return NavigationResult {
column_position: 0,
scroll_offset: 0,
description: "No columns available".to_string(),
viewport_changed: false,
};
}
let last_visual_column = total_visual_columns - 1;
self.crosshair_col = last_visual_column;
let available_width = self.terminal_width;
let pinned_count = self.dataview.get_pinned_columns().len();
let mut pinned_width = 0u16;
for i in 0..pinned_count {
let col_idx = display_columns[i];
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
pinned_width += width + 3; }
let available_for_scrollable = available_width.saturating_sub(pinned_width);
let mut accumulated_width = 0u16;
let mut new_scroll_offset = last_visual_column;
for visual_idx in (pinned_count..=last_visual_column).rev() {
let col_idx = display_columns[visual_idx];
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
col_idx,
);
accumulated_width += width + 3;
if accumulated_width > available_for_scrollable {
new_scroll_offset = visual_idx + 1;
break;
}
new_scroll_offset = visual_idx;
}
new_scroll_offset = new_scroll_offset.max(pinned_count);
let old_scroll_offset = self.viewport_cols.start;
let viewport_changed = old_scroll_offset != new_scroll_offset;
let visible_indices = self
.calculate_visible_column_indices_with_offset(self.terminal_width, new_scroll_offset);
let viewport_end = if let Some(&last_idx) = visible_indices.last() {
last_idx + 1
} else {
new_scroll_offset + 1
};
self.viewport_cols = new_scroll_offset..viewport_end;
debug!(target: "viewport_manager",
"navigate_to_last_column: last_visual={}, scroll_offset={}->{}",
last_visual_column, old_scroll_offset, new_scroll_offset);
NavigationResult {
column_position: last_visual_column,
scroll_offset: new_scroll_offset,
description: format!("Last column selected (column {})", last_visual_column + 1),
viewport_changed,
}
}
pub fn navigate_column_left(&mut self, current_display_position: usize) -> NavigationResult {
if self.viewport_lock {
debug!(target: "viewport_manager",
"navigate_column_left: Viewport locked, crosshair_col={}, viewport={:?}",
self.crosshair_col, self.viewport_cols);
if self.crosshair_col > self.viewport_cols.start {
self.crosshair_col -= 1;
return NavigationResult {
column_position: self.crosshair_col,
scroll_offset: self.viewport_cols.start,
description: "Moved within locked viewport".to_string(),
viewport_changed: false,
};
}
return NavigationResult {
column_position: self.crosshair_col,
scroll_offset: self.viewport_cols.start,
description: "At left edge of locked viewport".to_string(),
viewport_changed: false,
};
}
let display_columns = self.dataview.get_display_columns();
let total_display_columns = display_columns.len();
debug!(target: "viewport_manager",
"navigate_column_left: current_display_pos={}, total_display={}, display_order={:?}",
current_display_position, total_display_columns, display_columns);
let current_display_index = if current_display_position < total_display_columns {
current_display_position
} else {
0 };
debug!(target: "viewport_manager",
"navigate_column_left: using display_index={}",
current_display_index);
if current_display_index == 0 {
return NavigationResult {
column_position: 0, scroll_offset: self.viewport_cols.start,
description: "Already at first column".to_string(),
viewport_changed: false,
};
}
let new_display_index = current_display_index - 1;
let new_visual_column = display_columns
.get(new_display_index)
.copied()
.unwrap_or_else(|| {
display_columns
.get(current_display_index)
.copied()
.unwrap_or(0)
});
let old_scroll_offset = self.viewport_cols.start;
debug!(target: "viewport_manager",
"navigate_column_left: moving to datatable_column={}, current viewport={:?}",
new_visual_column, self.viewport_cols);
let viewport_changed = self.set_current_column(new_display_index);
let column_names = self.dataview.column_names();
let column_name = display_columns
.get(new_display_index)
.and_then(|&dt_idx| column_names.get(dt_idx))
.map_or("unknown", std::string::String::as_str);
let description = format!(
"Navigate left to column '{}' ({})",
column_name,
new_display_index + 1
);
debug!(target: "viewport_manager",
"navigate_column_left: display_pos {}→{}, datatable_col: {}, scroll: {}→{}, viewport_changed={}",
current_display_index, new_display_index, new_visual_column,
old_scroll_offset, self.viewport_cols.start, viewport_changed);
NavigationResult {
column_position: new_display_index, scroll_offset: self.viewport_cols.start,
description,
viewport_changed,
}
}
pub fn navigate_column_right(&mut self, current_display_position: usize) -> NavigationResult {
debug!(target: "viewport_manager",
"=== CRITICAL DEBUG: navigate_column_right CALLED ===");
debug!(target: "viewport_manager",
"Input current_display_position: {}", current_display_position);
debug!(target: "viewport_manager",
"Current crosshair_col: {}", self.crosshair_col);
debug!(target: "viewport_manager",
"Current viewport_cols: {:?}", self.viewport_cols);
if self.viewport_lock {
debug!(target: "viewport_manager",
"navigate_column_right: Viewport locked, crosshair_col={}, viewport={:?}",
self.crosshair_col, self.viewport_cols);
if self.crosshair_col < self.viewport_cols.end - 1 {
self.crosshair_col += 1;
return NavigationResult {
column_position: self.crosshair_col,
scroll_offset: self.viewport_cols.start,
description: "Moved within locked viewport".to_string(),
viewport_changed: false,
};
}
return NavigationResult {
column_position: self.crosshair_col,
scroll_offset: self.viewport_cols.start,
description: "At right edge of locked viewport".to_string(),
viewport_changed: false,
};
}
let display_columns = self.dataview.get_display_columns();
let total_display_columns = display_columns.len();
let column_names = self.dataview.column_names();
debug!(target: "viewport_manager",
"=== navigate_column_right DETAILED DEBUG ===");
debug!(target: "viewport_manager",
"ENTRY: current_display_pos={}, total_display_columns={}",
current_display_position, total_display_columns);
debug!(target: "viewport_manager",
"display_columns (DataTable indices): {:?}", display_columns);
if current_display_position < display_columns.len() {
let current_dt_idx = display_columns[current_display_position];
let current_name = column_names
.get(current_dt_idx)
.map_or("unknown", std::string::String::as_str);
debug!(target: "viewport_manager",
"Current position {} -> column '{}' (dt_idx={})",
current_display_position, current_name, current_dt_idx);
}
if current_display_position + 1 < display_columns.len() {
let next_dt_idx = display_columns[current_display_position + 1];
let next_name = column_names
.get(next_dt_idx)
.map_or("unknown", std::string::String::as_str);
debug!(target: "viewport_manager",
"Next position {} -> column '{}' (dt_idx={})",
current_display_position + 1, next_name, next_dt_idx);
}
let current_display_index = if current_display_position < total_display_columns {
current_display_position
} else {
debug!(target: "viewport_manager",
"WARNING: current_display_position {} >= total_display_columns {}, resetting to 0",
current_display_position, total_display_columns);
0 };
debug!(target: "viewport_manager",
"Validated: current_display_index={}",
current_display_index);
if current_display_index + 1 >= total_display_columns {
let last_display_index = total_display_columns.saturating_sub(1);
debug!(target: "viewport_manager",
"At last column boundary: current={}, total={}, returning last_display_index={}",
current_display_index, total_display_columns, last_display_index);
return NavigationResult {
column_position: last_display_index, scroll_offset: self.viewport_cols.start,
description: "Already at last column".to_string(),
viewport_changed: false,
};
}
let new_display_index = current_display_index + 1;
let new_visual_column = display_columns
.get(new_display_index)
.copied()
.unwrap_or_else(|| {
tracing::error!(
"[NAV_ERROR] Failed to get display column at index {}, total={}",
new_display_index,
display_columns.len()
);
display_columns
.get(current_display_index)
.copied()
.unwrap_or(0)
});
debug!(target: "viewport_manager",
"navigate_column_right: display_pos {}→{}, new_visual_column={}",
current_display_index, new_display_index, new_visual_column);
let old_scroll_offset = self.viewport_cols.start;
debug!(target: "viewport_manager",
"navigate_column_right: moving to datatable_column={}, current viewport={:?}",
new_visual_column, self.viewport_cols);
debug!(target: "viewport_manager",
"navigate_column_right: before set_current_column(visual_idx={}), viewport={:?}",
new_display_index, self.viewport_cols);
let viewport_changed = self.set_current_column(new_display_index);
debug!(target: "viewport_manager",
"navigate_column_right: after set_current_column(visual_idx={}), viewport={:?}, changed={}",
new_display_index, self.viewport_cols, viewport_changed);
let column_names = self.dataview.column_names();
let column_name = display_columns
.get(new_display_index)
.and_then(|&dt_idx| column_names.get(dt_idx))
.map_or("unknown", std::string::String::as_str);
let description = format!(
"Navigate right to column '{}' ({})",
column_name,
new_display_index + 1
);
debug!(target: "viewport_manager",
"=== navigate_column_right RESULT ===");
debug!(target: "viewport_manager",
"Returning: column_position={} (visual/display index)", new_display_index);
debug!(target: "viewport_manager",
"Movement: {} -> {} (visual indices)", current_display_index, new_display_index);
debug!(target: "viewport_manager",
"Viewport: {:?}, changed={}", self.viewport_cols, viewport_changed);
debug!(target: "viewport_manager",
"Description: {}", description);
tracing::debug!("[NAV_DEBUG] Final result: column_position={} (visual/display idx), viewport_changed={}",
new_display_index, viewport_changed);
debug!(target: "viewport_manager",
"navigate_column_right EXIT: display_pos {}→{}, datatable_col: {}, viewport: {:?}, scroll: {}→{}, viewport_changed={}",
current_display_index, new_display_index, new_visual_column,
self.viewport_cols, old_scroll_offset, self.viewport_cols.start, viewport_changed);
NavigationResult {
column_position: new_display_index, scroll_offset: self.viewport_cols.start,
description,
viewport_changed,
}
}
pub fn page_down(&mut self) -> RowNavigationResult {
let total_rows = self.dataview.row_count();
let visible_rows = self.terminal_height.saturating_sub(6) as usize;
debug!(target: "viewport_manager",
"page_down: crosshair_row={}, total_rows={}, visible_rows={}, current_viewport_rows={:?}",
self.crosshair_row, total_rows, visible_rows, self.viewport_rows);
if self.viewport_lock {
debug!(target: "viewport_manager",
"page_down: Viewport locked, moving within current viewport");
let new_row = self
.viewport_rows
.end
.saturating_sub(1)
.min(total_rows.saturating_sub(1));
self.crosshair_row = new_row;
return RowNavigationResult {
row_position: new_row,
row_scroll_offset: self.viewport_rows.start,
description: format!(
"Page down within locked viewport: row {} → {}",
self.crosshair_row + 1,
new_row + 1
),
viewport_changed: false,
};
}
if self.cursor_lock {
if let Some(lock_position) = self.cursor_lock_position {
debug!(target: "viewport_manager",
"page_down: Cursor locked at position {}", lock_position);
let old_scroll_offset = self.viewport_rows.start;
let max_scroll = total_rows.saturating_sub(visible_rows);
let new_scroll_offset = (old_scroll_offset + visible_rows).min(max_scroll);
if new_scroll_offset == old_scroll_offset {
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: old_scroll_offset,
description: "Already at bottom".to_string(),
viewport_changed: false,
};
}
self.viewport_rows =
new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
self.crosshair_row =
(new_scroll_offset + lock_position).min(total_rows.saturating_sub(1));
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: new_scroll_offset,
description: format!(
"Page down with cursor lock (viewport {} → {})",
old_scroll_offset + 1,
new_scroll_offset + 1
),
viewport_changed: true,
};
}
}
let new_row = (self.crosshair_row + visible_rows).min(total_rows.saturating_sub(1));
self.crosshair_row = new_row;
let old_scroll_offset = self.viewport_rows.start;
let new_scroll_offset = if new_row >= self.viewport_rows.start + visible_rows {
(new_row + 1).saturating_sub(visible_rows)
} else {
old_scroll_offset
};
self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
let viewport_changed = new_scroll_offset != old_scroll_offset;
let description = format!(
"Page down: row {} → {} (of {})",
self.crosshair_row + 1,
new_row + 1,
total_rows
);
debug!(target: "viewport_manager",
"page_down result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
RowNavigationResult {
row_position: new_row,
row_scroll_offset: new_scroll_offset,
description,
viewport_changed,
}
}
pub fn page_up(&mut self) -> RowNavigationResult {
let total_rows = self.dataview.row_count();
let visible_rows = self.terminal_height.saturating_sub(6) as usize;
debug!(target: "viewport_manager",
"page_up: crosshair_row={}, visible_rows={}, current_viewport_rows={:?}",
self.crosshair_row, visible_rows, self.viewport_rows);
if self.viewport_lock {
debug!(target: "viewport_manager",
"page_up: Viewport locked, moving within current viewport");
let new_row = self.viewport_rows.start;
self.crosshair_row = new_row;
return RowNavigationResult {
row_position: new_row,
row_scroll_offset: self.viewport_rows.start,
description: format!(
"Page up within locked viewport: row {} → {}",
self.crosshair_row + 1,
new_row + 1
),
viewport_changed: false,
};
}
if self.cursor_lock {
if let Some(lock_position) = self.cursor_lock_position {
debug!(target: "viewport_manager",
"page_up: Cursor locked at position {}", lock_position);
let old_scroll_offset = self.viewport_rows.start;
let new_scroll_offset = old_scroll_offset.saturating_sub(visible_rows);
if new_scroll_offset == old_scroll_offset {
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: old_scroll_offset,
description: "Already at top".to_string(),
viewport_changed: false,
};
}
self.viewport_rows =
new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
self.crosshair_row = new_scroll_offset + lock_position;
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: new_scroll_offset,
description: format!(
"Page up with cursor lock (viewport {} → {})",
old_scroll_offset + 1,
new_scroll_offset + 1
),
viewport_changed: true,
};
}
}
let new_row = self.crosshair_row.saturating_sub(visible_rows);
self.crosshair_row = new_row;
let old_scroll_offset = self.viewport_rows.start;
let new_scroll_offset = if new_row < self.viewport_rows.start {
new_row
} else {
old_scroll_offset
};
self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
let viewport_changed = new_scroll_offset != old_scroll_offset;
let description = format!("Page up: row {} → {}", self.crosshair_row + 1, new_row + 1);
debug!(target: "viewport_manager",
"page_up result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
RowNavigationResult {
row_position: new_row,
row_scroll_offset: new_scroll_offset,
description,
viewport_changed,
}
}
pub fn half_page_down(&mut self) -> RowNavigationResult {
let total_rows = self.dataview.row_count();
let visible_rows = self.terminal_height.saturating_sub(6) as usize; let half_page = visible_rows / 2;
debug!(target: "viewport_manager",
"half_page_down: crosshair_row={}, total_rows={}, half_page={}, current_viewport_rows={:?}",
self.crosshair_row, total_rows, half_page, self.viewport_rows);
if self.viewport_lock {
debug!(target: "viewport_manager",
"half_page_down: Viewport locked, moving within current viewport");
let new_row = self
.viewport_rows
.end
.saturating_sub(1)
.min(total_rows.saturating_sub(1));
self.crosshair_row = new_row;
return RowNavigationResult {
row_position: new_row,
row_scroll_offset: self.viewport_rows.start,
description: format!(
"Half page down within locked viewport: row {} → {}",
self.crosshair_row + 1,
new_row + 1
),
viewport_changed: false,
};
}
if self.cursor_lock {
if let Some(lock_position) = self.cursor_lock_position {
debug!(target: "viewport_manager",
"half_page_down: Cursor locked at position {}", lock_position);
let old_scroll_offset = self.viewport_rows.start;
let max_scroll = total_rows.saturating_sub(visible_rows);
let new_scroll_offset = (old_scroll_offset + half_page).min(max_scroll);
if new_scroll_offset == old_scroll_offset {
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: old_scroll_offset,
description: "Already at bottom".to_string(),
viewport_changed: false,
};
}
self.viewport_rows =
new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
self.crosshair_row =
(new_scroll_offset + lock_position).min(total_rows.saturating_sub(1));
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: new_scroll_offset,
description: format!(
"Half page down with cursor lock (viewport {} → {})",
old_scroll_offset + 1,
new_scroll_offset + 1
),
viewport_changed: true,
};
}
}
let new_row = (self.crosshair_row + half_page).min(total_rows.saturating_sub(1));
self.crosshair_row = new_row;
let old_scroll_offset = self.viewport_rows.start;
let new_scroll_offset = if new_row >= self.viewport_rows.start + visible_rows {
(new_row + 1).saturating_sub(visible_rows)
} else {
old_scroll_offset
};
self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
let viewport_changed = new_scroll_offset != old_scroll_offset;
let description = format!(
"Half page down: row {} → {} (of {})",
self.crosshair_row + 1 - half_page.min(self.crosshair_row),
new_row + 1,
total_rows
);
debug!(target: "viewport_manager",
"half_page_down result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
RowNavigationResult {
row_position: new_row,
row_scroll_offset: new_scroll_offset,
description,
viewport_changed,
}
}
pub fn half_page_up(&mut self) -> RowNavigationResult {
let total_rows = self.dataview.row_count();
let visible_rows = self.terminal_height.saturating_sub(6) as usize; let half_page = visible_rows / 2;
debug!(target: "viewport_manager",
"half_page_up: crosshair_row={}, half_page={}, current_viewport_rows={:?}",
self.crosshair_row, half_page, self.viewport_rows);
if self.viewport_lock {
debug!(target: "viewport_manager",
"half_page_up: Viewport locked, moving within current viewport");
let new_row = self.viewport_rows.start;
self.crosshair_row = new_row;
return RowNavigationResult {
row_position: new_row,
row_scroll_offset: self.viewport_rows.start,
description: format!(
"Half page up within locked viewport: row {} → {}",
self.crosshair_row + 1,
new_row + 1
),
viewport_changed: false,
};
}
if self.cursor_lock {
if let Some(lock_position) = self.cursor_lock_position {
debug!(target: "viewport_manager",
"half_page_up: Cursor locked at position {}", lock_position);
let old_scroll_offset = self.viewport_rows.start;
let new_scroll_offset = old_scroll_offset.saturating_sub(half_page);
if new_scroll_offset == old_scroll_offset {
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: old_scroll_offset,
description: "Already at top".to_string(),
viewport_changed: false,
};
}
self.viewport_rows =
new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
self.crosshair_row = new_scroll_offset + lock_position;
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: new_scroll_offset,
description: format!(
"Half page up with cursor lock (viewport {} → {})",
old_scroll_offset + 1,
new_scroll_offset + 1
),
viewport_changed: true,
};
}
}
let new_row = self.crosshair_row.saturating_sub(half_page);
self.crosshair_row = new_row;
let old_scroll_offset = self.viewport_rows.start;
let new_scroll_offset = if new_row < self.viewport_rows.start {
new_row
} else {
old_scroll_offset
};
self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
let viewport_changed = new_scroll_offset != old_scroll_offset;
let description = format!(
"Half page up: row {} → {}",
self.crosshair_row + half_page + 1,
new_row + 1
);
debug!(target: "viewport_manager",
"half_page_up result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
RowNavigationResult {
row_position: new_row,
row_scroll_offset: new_scroll_offset,
description,
viewport_changed,
}
}
pub fn navigate_to_last_row(&mut self, total_rows: usize) -> RowNavigationResult {
if self.viewport_lock {
let last_visible = self
.viewport_rows
.end
.saturating_sub(1)
.min(total_rows.saturating_sub(1));
self.crosshair_row = last_visible;
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description: "Moved to last visible row (viewport locked)".to_string(),
viewport_changed: false,
};
}
if total_rows == 0 {
return RowNavigationResult {
row_position: 0,
row_scroll_offset: 0,
description: "No rows to navigate".to_string(),
viewport_changed: false,
};
}
let visible_rows = (self.terminal_height as usize).max(10);
let last_row = total_rows - 1;
let new_scroll_offset = total_rows.saturating_sub(visible_rows);
debug!(target: "viewport_manager",
"navigate_to_last_row: total_rows={}, last_row={}, visible_rows={}, new_scroll_offset={}",
total_rows, last_row, visible_rows, new_scroll_offset);
let old_scroll_offset = self.viewport_rows.start;
let viewport_changed = new_scroll_offset != old_scroll_offset;
self.viewport_rows = new_scroll_offset..total_rows.min(new_scroll_offset + visible_rows);
self.crosshair_row = last_row;
let description = format!("Jumped to last row ({}/{})", last_row + 1, total_rows);
debug!(target: "viewport_manager",
"navigate_to_last_row result: row={}, crosshair_row={}, scroll_offset={}→{}, viewport_changed={}",
last_row, self.crosshair_row, old_scroll_offset, new_scroll_offset, viewport_changed);
RowNavigationResult {
row_position: last_row,
row_scroll_offset: new_scroll_offset,
description,
viewport_changed,
}
}
pub fn navigate_to_first_row(&mut self, total_rows: usize) -> RowNavigationResult {
if self.viewport_lock {
self.crosshair_row = self.viewport_rows.start;
return RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description: "Moved to first visible row (viewport locked)".to_string(),
viewport_changed: false,
};
}
if total_rows == 0 {
return RowNavigationResult {
row_position: 0,
row_scroll_offset: 0,
description: "No rows to navigate".to_string(),
viewport_changed: false,
};
}
let visible_rows = (self.terminal_height as usize).max(10);
let first_row = 0;
let new_scroll_offset = 0;
debug!(target: "viewport_manager",
"navigate_to_first_row: total_rows={}, visible_rows={}",
total_rows, visible_rows);
let old_scroll_offset = self.viewport_rows.start;
let viewport_changed = new_scroll_offset != old_scroll_offset;
self.viewport_rows = 0..visible_rows.min(total_rows);
self.crosshair_row = first_row;
let description = format!("Jumped to first row (1/{total_rows})");
debug!(target: "viewport_manager",
"navigate_to_first_row result: row=0, crosshair_row={}, scroll_offset={}→0, viewport_changed={}",
self.crosshair_row, old_scroll_offset, viewport_changed);
RowNavigationResult {
row_position: first_row,
row_scroll_offset: new_scroll_offset,
description,
viewport_changed,
}
}
pub fn navigate_to_viewport_top(&mut self) -> RowNavigationResult {
let top_row = self.viewport_rows.start;
let old_row = self.crosshair_row;
self.crosshair_row = top_row;
let description = format!("Moved to viewport top (row {})", top_row + 1);
debug!(target: "viewport_manager",
"navigate_to_viewport_top: crosshair {} -> {}",
old_row, self.crosshair_row);
RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description,
viewport_changed: false, }
}
pub fn navigate_to_viewport_middle(&mut self) -> RowNavigationResult {
let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
let middle_offset = viewport_height / 2;
let middle_row = self.viewport_rows.start + middle_offset;
let old_row = self.crosshair_row;
self.crosshair_row = middle_row;
let description = format!("Moved to viewport middle (row {})", middle_row + 1);
debug!(target: "viewport_manager",
"navigate_to_viewport_middle: crosshair {} -> {}",
old_row, self.crosshair_row);
RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description,
viewport_changed: false, }
}
pub fn navigate_to_viewport_bottom(&mut self) -> RowNavigationResult {
let bottom_row = self.viewport_rows.end.saturating_sub(1);
let old_row = self.crosshair_row;
self.crosshair_row = bottom_row;
let description = format!("Moved to viewport bottom (row {})", bottom_row + 1);
debug!(target: "viewport_manager",
"navigate_to_viewport_bottom: crosshair {} -> {}",
old_row, self.crosshair_row);
RowNavigationResult {
row_position: self.crosshair_row,
row_scroll_offset: self.viewport_rows.start,
description,
viewport_changed: false, }
}
pub fn toggle_cursor_lock(&mut self) -> (bool, String) {
self.cursor_lock = !self.cursor_lock;
if self.cursor_lock {
let relative_position = self.crosshair_row.saturating_sub(self.viewport_rows.start);
self.cursor_lock_position = Some(relative_position);
let description = format!(
"Cursor lock: ON (locked at viewport position {})",
relative_position + 1
);
debug!(target: "viewport_manager",
"Cursor lock enabled: crosshair at viewport position {}",
relative_position);
(true, description)
} else {
self.cursor_lock_position = None;
let description = "Cursor lock: OFF".to_string();
debug!(target: "viewport_manager", "Cursor lock disabled");
(false, description)
}
}
pub fn toggle_viewport_lock(&mut self) -> (bool, String) {
self.viewport_lock = !self.viewport_lock;
if self.viewport_lock {
self.viewport_lock_boundaries = Some(self.viewport_rows.clone());
let description = format!(
"Viewport lock: ON (no scrolling, cursor constrained to rows {}-{})",
self.viewport_rows.start + 1,
self.viewport_rows.end
);
debug!(target: "viewport_manager",
"VIEWPORT LOCK ENABLED: boundaries {:?}, crosshair={}, viewport={:?}",
self.viewport_lock_boundaries, self.crosshair_row, self.viewport_rows);
(true, description)
} else {
self.viewport_lock_boundaries = None;
let description = "Viewport lock: OFF (normal scrolling)".to_string();
debug!(target: "viewport_manager", "VIEWPORT LOCK DISABLED");
(false, description)
}
}
#[must_use]
pub fn is_cursor_locked(&self) -> bool {
self.cursor_lock
}
#[must_use]
pub fn is_viewport_locked(&self) -> bool {
self.viewport_lock
}
pub fn lock_viewport(&mut self) {
if !self.viewport_lock {
self.viewport_lock = true;
self.viewport_lock_boundaries = Some(self.viewport_rows.clone());
debug!(target: "viewport_manager", "Viewport locked: rows {}-{}",
self.viewport_rows.start + 1, self.viewport_rows.end);
}
}
pub fn unlock_viewport(&mut self) {
if self.viewport_lock {
self.viewport_lock = false;
self.viewport_lock_boundaries = None;
debug!(target: "viewport_manager", "Viewport unlocked");
}
}
pub fn reorder_column_left(&mut self, current_column: usize) -> ColumnReorderResult {
debug!(target: "viewport_manager",
"reorder_column_left: current_column={}, viewport={:?}",
current_column, self.viewport_cols
);
let column_count = self.dataview.column_count();
if current_column >= column_count {
return ColumnReorderResult {
new_column_position: current_column,
description: "Invalid column position".to_string(),
success: false,
};
}
let pinned_count = self.dataview.get_pinned_columns().len();
debug!(target: "viewport_manager",
"Before move: column_count={}, pinned_count={}, current_column={}",
column_count, pinned_count, current_column
);
let mut new_dataview = (*self.dataview).clone();
let success = new_dataview.move_column_left(current_column);
if success {
self.dataview = Arc::new(new_dataview);
}
if success {
self.invalidate_cache();
let wrapped_to_end =
current_column == 0 || (current_column == pinned_count && pinned_count > 0);
let new_position = if wrapped_to_end {
column_count - 1
} else {
current_column - 1
};
let column_names = self.dataview.column_names();
let column_name = column_names
.get(new_position)
.map_or("?", std::string::String::as_str);
debug!(target: "viewport_manager",
"After move: new_position={}, wrapped_to_end={}, column_name={}",
new_position, wrapped_to_end, column_name
);
if wrapped_to_end {
let optimal_offset = self.calculate_optimal_offset_for_last_column(
self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH),
);
debug!(target: "viewport_manager",
"Column wrapped to end! Adjusting viewport from {:?} to {}..{}",
self.viewport_cols, optimal_offset, self.dataview.column_count()
);
self.viewport_cols = optimal_offset..self.dataview.column_count();
} else {
if !self.viewport_cols.contains(&new_position) {
let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
let columns_that_fit =
self.calculate_columns_that_fit(new_position, terminal_width);
let new_start = if new_position < self.viewport_cols.start {
new_position
} else {
new_position.saturating_sub(columns_that_fit - 1)
};
let new_end = (new_start + columns_that_fit).min(self.dataview.column_count());
self.viewport_cols = new_start..new_end;
debug!(target: "viewport_manager",
"Column moved outside viewport! Adjusting viewport to {}..{} to show column {} at position {}",
new_start, new_end, column_name, new_position
);
}
}
self.crosshair_col = new_position;
ColumnReorderResult {
new_column_position: new_position,
description: format!("Moved column '{column_name}' left"),
success: true,
}
} else {
ColumnReorderResult {
new_column_position: current_column,
description: "Cannot move column left".to_string(),
success: false,
}
}
}
pub fn reorder_column_right(&mut self, current_column: usize) -> ColumnReorderResult {
let column_count = self.dataview.column_count();
if current_column >= column_count {
return ColumnReorderResult {
new_column_position: current_column,
description: "Invalid column position".to_string(),
success: false,
};
}
let pinned_count = self.dataview.get_pinned_columns().len();
let mut new_dataview = (*self.dataview).clone();
let success = new_dataview.move_column_right(current_column);
if success {
self.dataview = Arc::new(new_dataview);
}
if success {
self.invalidate_cache();
let wrapped_to_beginning = current_column == column_count - 1
|| (pinned_count > 0 && current_column == pinned_count - 1);
let new_position = if current_column == column_count - 1 {
if pinned_count > 0 {
pinned_count } else {
0 }
} else if pinned_count > 0 && current_column == pinned_count - 1 {
0
} else {
current_column + 1
};
let column_names = self.dataview.column_names();
let column_name = column_names
.get(new_position)
.map_or("?", std::string::String::as_str);
if wrapped_to_beginning {
self.viewport_cols = 0..self.dataview.column_count().min(20); debug!(target: "viewport_manager",
"Column wrapped to beginning, resetting viewport to show column {} at position {}",
column_name, new_position
);
} else {
if !self.viewport_cols.contains(&new_position) {
let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
let columns_that_fit =
self.calculate_columns_that_fit(new_position, terminal_width);
let new_start = if new_position > self.viewport_cols.end {
new_position.saturating_sub(columns_that_fit - 1)
} else {
new_position
};
let new_end = (new_start + columns_that_fit).min(self.dataview.column_count());
self.viewport_cols = new_start..new_end;
debug!(target: "viewport_manager",
"Column moved outside viewport! Adjusting viewport to {}..{} to show column {} at position {}",
new_start, new_end, column_name, new_position
);
}
}
self.crosshair_col = new_position;
ColumnReorderResult {
new_column_position: new_position,
description: format!("Moved column '{column_name}' right"),
success: true,
}
} else {
ColumnReorderResult {
new_column_position: current_column,
description: "Cannot move column right".to_string(),
success: false,
}
}
}
pub fn hide_column(&mut self, column_index: usize) -> bool {
debug!(target: "viewport_manager", "hide_column: column_index={}", column_index);
let mut new_dataview = (*self.dataview).clone();
let success = new_dataview.hide_column(column_index);
if success {
self.dataview = Arc::new(new_dataview);
self.invalidate_cache();
let column_count = self.dataview.column_count();
if self.viewport_cols.end > column_count {
self.viewport_cols.end = column_count;
}
if self.viewport_cols.start >= column_count && column_count > 0 {
self.viewport_cols.start = column_count - 1;
}
if column_index == self.crosshair_col {
if column_count > 0 {
if self.crosshair_col >= column_count {
self.crosshair_col = column_count - 1;
}
} else {
self.crosshair_col = 0;
}
debug!(target: "viewport_manager", "Crosshair was on hidden column, moved to {}", self.crosshair_col);
} else if column_index < self.crosshair_col {
self.crosshair_col = self.crosshair_col.saturating_sub(1);
debug!(target: "viewport_manager", "Hidden column was before crosshair, adjusted crosshair to {}", self.crosshair_col);
}
debug!(target: "viewport_manager", "Column {} hidden successfully", column_index);
} else {
debug!(target: "viewport_manager", "Failed to hide column {} (might be pinned)", column_index);
}
success
}
pub fn hide_column_by_name(&mut self, column_name: &str) -> bool {
debug!(target: "viewport_manager", "hide_column_by_name: column_name={}", column_name);
let mut new_dataview = (*self.dataview).clone();
let success = new_dataview.hide_column_by_name(column_name);
if success {
self.dataview = Arc::new(new_dataview);
}
if success {
self.invalidate_cache();
let column_count = self.dataview.column_count();
if self.viewport_cols.end > column_count {
self.viewport_cols.end = column_count;
}
if self.viewport_cols.start >= column_count && column_count > 0 {
self.viewport_cols.start = column_count - 1;
}
if self.crosshair_col >= column_count && column_count > 0 {
self.crosshair_col = column_count - 1;
debug!(target: "viewport_manager", "Adjusted crosshair to {} after hiding column", self.crosshair_col);
}
debug!(target: "viewport_manager", "Column '{}' hidden successfully", column_name);
} else {
debug!(target: "viewport_manager", "Failed to hide column '{}' (might be pinned or not found)", column_name);
}
success
}
pub fn hide_empty_columns(&mut self) -> usize {
debug!(target: "viewport_manager", "hide_empty_columns called");
let mut new_dataview = (*self.dataview).clone();
let count = new_dataview.hide_empty_columns();
if count > 0 {
self.dataview = Arc::new(new_dataview);
}
if count > 0 {
self.invalidate_cache();
let column_count = self.dataview.column_count();
if self.viewport_cols.end > column_count {
self.viewport_cols.end = column_count;
}
if self.viewport_cols.start >= column_count && column_count > 0 {
self.viewport_cols.start = column_count - 1;
}
debug!(target: "viewport_manager", "Hidden {} empty columns", count);
}
count
}
pub fn unhide_all_columns(&mut self) {
debug!(target: "viewport_manager", "unhide_all_columns called");
let mut new_dataview = (*self.dataview).clone();
new_dataview.unhide_all_columns();
self.dataview = Arc::new(new_dataview);
self.invalidate_cache();
let column_count = self.dataview.column_count();
self.viewport_cols = 0..column_count.min(20);
debug!(target: "viewport_manager", "All columns unhidden, viewport reset to {:?}", self.viewport_cols);
}
pub fn pin_column(&mut self, column_index: usize) -> bool {
debug!(target: "viewport_manager", "pin_column: column_index={}", column_index);
let mut new_dataview = (*self.dataview).clone();
let success = new_dataview.pin_column(column_index).is_ok();
if success {
self.dataview = Arc::new(new_dataview);
self.invalidate_cache();
debug!(target: "viewport_manager", "Column {} pinned successfully", column_index);
} else {
debug!(target: "viewport_manager", "Failed to pin column {}", column_index);
}
success
}
pub fn set_current_column(&mut self, visual_column: usize) -> bool {
let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH); let total_visual_columns = self.dataview.get_display_columns().len();
tracing::debug!("[PIN_DEBUG] === set_current_column ===");
tracing::debug!(
"[PIN_DEBUG] visual_column={}, viewport_cols={:?}",
visual_column,
self.viewport_cols
);
tracing::debug!(
"[PIN_DEBUG] terminal_width={}, total_visual_columns={}",
terminal_width,
total_visual_columns
);
debug!(target: "viewport_manager",
"set_current_column ENTRY: visual_column={}, current_viewport={:?}, terminal_width={}, total_visual={}",
visual_column, self.viewport_cols, terminal_width, total_visual_columns);
if visual_column >= total_visual_columns {
debug!(target: "viewport_manager", "Visual column {} out of bounds (max {})", visual_column, total_visual_columns);
tracing::debug!(
"[PIN_DEBUG] Column {} out of bounds (max {})",
visual_column,
total_visual_columns
);
return false;
}
self.crosshair_col = visual_column;
debug!(target: "viewport_manager", "Updated crosshair_col to {}", visual_column);
tracing::debug!("[PIN_DEBUG] Updated crosshair_col to {}", visual_column);
let display_columns = self.dataview.get_display_columns();
let mut total_width_needed = 0u16;
for &dt_idx in &display_columns {
let width =
self.width_calculator
.get_column_width(&self.dataview, &self.viewport_rows, dt_idx);
total_width_needed += width + 1; }
if total_width_needed <= terminal_width {
debug!(target: "viewport_manager",
"Visual column {} in optimal layout mode (all columns fit), no adjustment needed", visual_column);
tracing::debug!("[PIN_DEBUG] All columns fit, no adjustment needed");
tracing::debug!("[PIN_DEBUG] === End set_current_column (all fit) ===");
return false;
}
let pinned_count = self.dataview.get_pinned_columns().len();
tracing::debug!("[PIN_DEBUG] pinned_count={}", pinned_count);
let visible_columns = self.calculate_visible_column_indices(terminal_width);
let display_columns = self.dataview.get_display_columns();
let target_dt_idx = if visual_column < display_columns.len() {
display_columns[visual_column]
} else {
tracing::debug!("[PIN_DEBUG] Column {} out of bounds", visual_column);
return false;
};
let is_visible = visible_columns.contains(&target_dt_idx);
tracing::debug!(
"[PIN_DEBUG] Column {} (dt_idx={}) visible check: visible_columns={:?}, is_visible={}",
visual_column,
target_dt_idx,
visible_columns,
is_visible
);
debug!(target: "viewport_manager",
"set_current_column CHECK: visual_column={}, viewport={:?}, is_visible={}",
visual_column, self.viewport_cols, is_visible);
if is_visible {
debug!(target: "viewport_manager", "Visual column {} already visible in viewport {:?}, no adjustment needed",
visual_column, self.viewport_cols);
tracing::debug!("[PIN_DEBUG] Column already visible, no adjustment");
tracing::debug!("[PIN_DEBUG] === End set_current_column (no change) ===");
return false;
}
debug!(target: "viewport_manager", "Visual column {} NOT visible, calculating new offset", visual_column);
let new_scroll_offset = self.calculate_scroll_offset_for_visual_column(visual_column);
let old_scroll_offset = self.viewport_cols.start;
debug!(target: "viewport_manager", "Calculated new_scroll_offset={}, old_scroll_offset={}",
new_scroll_offset, old_scroll_offset);
if new_scroll_offset != old_scroll_offset {
let display_columns = self.dataview.get_display_columns();
let pinned_count = self.dataview.get_pinned_columns().len();
let mut used_width = 0u16;
let separator_width = 1u16;
for visual_idx in 0..pinned_count {
if visual_idx < display_columns.len() {
let dt_idx = display_columns[visual_idx];
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
dt_idx,
);
used_width += width + separator_width;
}
}
let mut scrollable_columns_that_fit = 0;
let visual_start = pinned_count + new_scroll_offset;
for visual_idx in visual_start..display_columns.len() {
let dt_idx = display_columns[visual_idx];
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
dt_idx,
);
if used_width + width + separator_width <= terminal_width {
used_width += width + separator_width;
scrollable_columns_that_fit += 1;
} else {
break;
}
}
let new_end = new_scroll_offset + scrollable_columns_that_fit;
self.viewport_cols = new_scroll_offset..new_end;
self.cache_dirty = true;
debug!(target: "viewport_manager",
"Adjusted viewport for visual column {}: offset {}→{} (viewport: {:?})",
visual_column, old_scroll_offset, new_scroll_offset, self.viewport_cols);
return true;
}
false
}
fn calculate_visible_column_indices_with_offset(
&mut self,
available_width: u16,
scroll_offset: usize,
) -> Vec<usize> {
let original_viewport = self.viewport_cols.clone();
let total_visual_columns = self.dataview.get_display_columns().len();
self.viewport_cols = scroll_offset..(scroll_offset + 50).min(total_visual_columns);
let visible_columns = self.calculate_visible_column_indices(available_width);
self.viewport_cols = original_viewport;
visible_columns
}
fn calculate_scroll_offset_for_visual_column(&mut self, visual_column: usize) -> usize {
debug!(target: "viewport_manager",
"=== calculate_scroll_offset_for_visual_column ENTRY ===");
debug!(target: "viewport_manager",
"visual_column={}, current_viewport={:?}", visual_column, self.viewport_cols);
let pinned_count = self.dataview.get_pinned_columns().len();
debug!(target: "viewport_manager",
"pinned_count={}", pinned_count);
if visual_column < pinned_count {
debug!(target: "viewport_manager",
"Visual column {} is pinned, returning current offset {}",
visual_column, self.viewport_cols.start);
return self.viewport_cols.start; }
let scrollable_column = visual_column - pinned_count;
debug!(target: "viewport_manager",
"Converted to scrollable_column={}", scrollable_column);
let current_scroll_offset = self.viewport_cols.start;
let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
let display_columns = self.dataview.get_display_columns();
let mut pinned_width = 0u16;
let separator_width = 1u16;
for visual_idx in 0..pinned_count {
if visual_idx < display_columns.len() {
let dt_idx = display_columns[visual_idx];
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
dt_idx,
);
pinned_width += width + separator_width;
}
}
let available_for_scrollable = terminal_width.saturating_sub(pinned_width);
debug!(target: "viewport_manager",
"Scroll offset calculation: target_scrollable_col={}, current_offset={}, available_width={}",
scrollable_column, current_scroll_offset, available_for_scrollable);
if scrollable_column < current_scroll_offset {
debug!(target: "viewport_manager", "Column {} is left of viewport, scrolling left to offset {}",
scrollable_column, scrollable_column);
scrollable_column
} else {
debug!(target: "viewport_manager",
"Checking if column {} can be made visible with minimal scrolling from offset {}",
scrollable_column, current_scroll_offset);
let mut test_scroll_offset = current_scroll_offset;
let max_scrollable_columns = display_columns.len().saturating_sub(pinned_count);
while test_scroll_offset <= scrollable_column
&& test_scroll_offset < max_scrollable_columns
{
let mut used_width = 0u16;
let mut target_column_fits = false;
for test_scrollable_idx in test_scroll_offset..max_scrollable_columns {
let visual_idx = pinned_count + test_scrollable_idx;
if visual_idx < display_columns.len() {
let dt_idx = display_columns[visual_idx];
let width = self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
dt_idx,
);
if used_width + width + separator_width <= available_for_scrollable {
used_width += width + separator_width;
if test_scrollable_idx == scrollable_column {
target_column_fits = true;
break; }
} else {
break; }
}
}
debug!(target: "viewport_manager",
"Testing scroll_offset={}: target_fits={}, used_width={}",
test_scroll_offset, target_column_fits, used_width);
if target_column_fits {
debug!(target: "viewport_manager",
"Found minimal scroll offset {} for column {} (current was {})",
test_scroll_offset, scrollable_column, current_scroll_offset);
return test_scroll_offset;
}
test_scroll_offset += 1;
}
debug!(target: "viewport_manager",
"Could not find minimal scroll, placing column {} at scroll offset {}",
scrollable_column, scrollable_column);
scrollable_column
}
}
pub fn goto_line(&mut self, target_row: usize) -> RowNavigationResult {
let total_rows = self.dataview.row_count();
let target_row = target_row.min(total_rows.saturating_sub(1));
let visible_rows = (self.terminal_height as usize).saturating_sub(6);
let centered_scroll_offset = if visible_rows > 0 {
let half_viewport = visible_rows / 2;
if target_row > half_viewport {
(target_row - half_viewport).min(total_rows.saturating_sub(visible_rows))
} else {
0
}
} else {
target_row
};
let old_scroll_offset = self.viewport_rows.start;
self.viewport_rows =
centered_scroll_offset..(centered_scroll_offset + visible_rows).min(total_rows);
let viewport_changed = centered_scroll_offset != old_scroll_offset;
self.crosshair_row = target_row;
let description = format!(
"Jumped to row {} (centered at viewport {})",
target_row + 1,
centered_scroll_offset + 1
);
debug!(target: "viewport_manager",
"goto_line: target_row={}, crosshair_row={}, scroll_offset={}→{}, viewport={:?}",
target_row, self.crosshair_row, old_scroll_offset, centered_scroll_offset, self.viewport_rows);
RowNavigationResult {
row_position: target_row,
row_scroll_offset: centered_scroll_offset,
description,
viewport_changed,
}
}
pub fn hide_current_column_with_result(&mut self) -> ColumnOperationResult {
let visual_col_idx = self.get_crosshair_col();
let columns = self.dataview.column_names();
if visual_col_idx >= columns.len() {
return ColumnOperationResult::failure("Invalid column position");
}
let col_name = columns[visual_col_idx].clone();
let visible_count = columns.len();
if visible_count <= 1 {
return ColumnOperationResult::failure("Cannot hide the last visible column");
}
let success = self.hide_column(visual_col_idx);
if success {
let mut result = ColumnOperationResult::success(format!("Column '{col_name}' hidden"));
result.updated_dataview = Some(self.clone_dataview());
result.new_column_position = Some(self.get_crosshair_col());
result.new_viewport = Some(self.viewport_cols.clone());
result
} else {
ColumnOperationResult::failure(format!(
"Cannot hide column '{col_name}' (may be pinned)"
))
}
}
pub fn unhide_all_columns_with_result(&mut self) -> ColumnOperationResult {
let hidden_columns = self.dataview.get_hidden_column_names();
let count = hidden_columns.len();
if count == 0 {
return ColumnOperationResult::success("No hidden columns");
}
self.unhide_all_columns();
let mut result = ColumnOperationResult::success(format!("Unhidden {count} column(s)"));
result.updated_dataview = Some(self.clone_dataview());
result.affected_count = Some(count);
result.new_viewport = Some(self.viewport_cols.clone());
result
}
pub fn reorder_column_left_with_result(&mut self) -> ColumnOperationResult {
let current_col = self.get_crosshair_col();
let reorder_result = self.reorder_column_left(current_col);
if reorder_result.success {
let mut result = ColumnOperationResult::success(reorder_result.description);
result.updated_dataview = Some(self.clone_dataview());
result.new_column_position = Some(reorder_result.new_column_position);
result.new_viewport = Some(self.viewport_cols.clone());
result
} else {
ColumnOperationResult::failure(reorder_result.description)
}
}
pub fn reorder_column_right_with_result(&mut self) -> ColumnOperationResult {
let current_col = self.get_crosshair_col();
let reorder_result = self.reorder_column_right(current_col);
if reorder_result.success {
let mut result = ColumnOperationResult::success(reorder_result.description);
result.updated_dataview = Some(self.clone_dataview());
result.new_column_position = Some(reorder_result.new_column_position);
result.new_viewport = Some(self.viewport_cols.clone());
result
} else {
ColumnOperationResult::failure(reorder_result.description)
}
}
pub fn calculate_viewport_column_widths(
&mut self,
viewport_start: usize,
viewport_end: usize,
compact_mode: bool,
) -> Vec<u16> {
let headers = self.dataview.column_names();
let mut widths = Vec::with_capacity(headers.len());
let min_width = if compact_mode { 4 } else { 6 };
let padding = if compact_mode { 1 } else { 2 };
let available_width = self.terminal_width.saturating_sub(10) as usize;
let visible_cols = headers.len().min(12);
let dynamic_max = if visible_cols > 0 {
(available_width / visible_cols).max(30).min(80)
} else {
30
};
let max_width = if compact_mode {
dynamic_max.min(40)
} else {
dynamic_max
};
let mut rows_to_check = Vec::new();
let source_table = self.dataview.source();
for i in viewport_start..viewport_end.min(source_table.row_count()) {
if let Some(row_strings) = source_table.get_row_as_strings(i) {
rows_to_check.push(row_strings);
}
}
for (col_idx, header) in headers.iter().enumerate() {
let mut max_col_width = header.len();
for row in &rows_to_check {
if let Some(value) = row.get(col_idx) {
let display_value = if value.is_empty() {
"NULL"
} else {
value.as_str()
};
max_col_width = max_col_width.max(display_value.len());
}
}
let width = (max_col_width + padding).clamp(min_width, max_width) as u16;
widths.push(width);
}
widths
}
pub fn calculate_optimal_column_widths(&mut self) -> Vec<u16> {
self.width_calculator.calculate_with_terminal_width(
&self.dataview,
&self.viewport_rows,
self.terminal_width,
);
let col_count = self.dataview.column_count();
let mut widths = Vec::with_capacity(col_count);
for idx in 0..col_count {
widths.push(self.width_calculator.get_column_width(
&self.dataview,
&self.viewport_rows,
idx,
));
}
widths
}
pub fn ensure_column_visible(&mut self, column_index: usize, available_width: u16) {
debug!(target: "viewport_manager", "ensure_column_visible: column_index={}, available_width={}", column_index, available_width);
let total_columns = self.dataview.get_display_columns().len();
if column_index >= total_columns {
debug!(target: "viewport_manager", "Column index {} out of range (max {})", column_index, total_columns.saturating_sub(1));
return;
}
let visible_columns = self.calculate_visible_column_indices(available_width);
let dt_columns = self.dataview.get_display_columns();
if let Some(&dt_index) = dt_columns.get(column_index) {
if visible_columns.contains(&dt_index) {
debug!(target: "viewport_manager", "Column {} already visible", column_index);
return;
}
}
if self.set_current_column(column_index) {
self.crosshair_col = column_index;
debug!(target: "viewport_manager", "Ensured column {} is visible and set crosshair", column_index);
} else {
debug!(target: "viewport_manager", "Failed to make column {} visible", column_index);
}
}
pub fn reorder_column(&mut self, from_index: usize, to_index: usize) -> bool {
debug!(target: "viewport_manager", "reorder_column: from_index={}, to_index={}", from_index, to_index);
if from_index == to_index {
return true; }
let mut new_dataview = (*self.dataview).clone();
let mut current_pos = from_index;
let mut success = true;
if from_index < to_index {
while current_pos < to_index && success {
success = new_dataview.move_column_right(current_pos);
if success {
current_pos += 1;
}
}
} else {
while current_pos > to_index && success {
success = new_dataview.move_column_left(current_pos);
if success {
current_pos -= 1;
}
}
}
if success {
self.dataview = Arc::new(new_dataview);
self.invalidate_cache();
debug!(target: "viewport_manager", "Column moved from {} to {} successfully", from_index, to_index);
} else {
debug!(target: "viewport_manager", "Failed to move column from {} to {}", from_index, to_index);
}
success
}
pub fn calculate_column_widths(&mut self, available_width: u16) -> Vec<u16> {
let _visible_indices = self.calculate_visible_column_indices(available_width);
self.get_column_widths().to_vec()
}
}
#[derive(Debug, Clone)]
pub struct ViewportEfficiency {
pub available_width: u16,
pub used_width: u16,
pub wasted_space: u16,
pub efficiency_percent: u8,
pub visible_columns: usize,
pub column_widths: Vec<u16>,
pub next_column_width: Option<u16>, pub columns_that_could_fit: Vec<(usize, u16)>, }
impl ViewportEfficiency {
#[must_use]
pub fn to_status_string(&self) -> String {
format!(
"Viewport: {}w used of {}w ({}% efficient, {} cols, {}w wasted)",
self.used_width,
self.available_width,
self.efficiency_percent,
self.visible_columns,
self.wasted_space
)
}
#[must_use]
pub fn to_debug_string(&self) -> String {
let avg_width = if self.column_widths.is_empty() {
0
} else {
self.column_widths.iter().sum::<u16>() / self.column_widths.len() as u16
};
let mut efficiency_analysis = String::new();
if let Some(next_width) = self.next_column_width {
efficiency_analysis.push_str(&format!(
"\n\n Next column needs: {next_width}w (+1 separator)"
));
if next_width < self.wasted_space {
efficiency_analysis.push_str(" ✓ FITS!");
} else {
efficiency_analysis.push_str(&format!(" ✗ Too wide (have {}w)", self.wasted_space));
}
}
if !self.columns_that_could_fit.is_empty() {
efficiency_analysis.push_str(&format!(
"\n Columns that COULD fit in {}w:",
self.wasted_space
));
for (idx, width) in
&self.columns_that_could_fit[..self.columns_that_could_fit.len().min(5)]
{
efficiency_analysis.push_str(&format!("\n - Column {idx}: {width}w"));
}
if self.columns_that_could_fit.len() > 5 {
efficiency_analysis.push_str(&format!(
"\n ... and {} more",
self.columns_that_could_fit.len() - 5
));
}
}
efficiency_analysis.push_str("\n\n Hypothetical efficiencies:");
for extra in 1..=3 {
let hypothetical_used =
self.used_width + (extra * (avg_width + 1)).min(self.wasted_space);
let hypothetical_eff =
((f32::from(hypothetical_used) / f32::from(self.available_width)) * 100.0) as u8;
let hypothetical_wasted = self.available_width.saturating_sub(hypothetical_used);
efficiency_analysis.push_str(&format!(
"\n +{extra} cols ({avg_width}w each): {hypothetical_eff}% efficiency, {hypothetical_wasted}w wasted"
));
}
format!(
"Viewport Efficiency:\n Available: {}w\n Used: {}w\n Wasted: {}w\n Efficiency: {}%\n Columns: {} visible\n Widths: {:?}\n Avg Width: {}w{}",
self.available_width,
self.used_width,
self.wasted_space,
self.efficiency_percent,
self.visible_columns,
self.column_widths.clone(),
avg_width,
efficiency_analysis
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::datatable::{DataColumn, DataRow, DataTable, DataValue};
fn create_test_dataview() -> Arc<DataView> {
let mut table = DataTable::new("test");
table.add_column(DataColumn::new("id"));
table.add_column(DataColumn::new("name"));
table.add_column(DataColumn::new("amount"));
for i in 0..100 {
table
.add_row(DataRow::new(vec![
DataValue::Integer(i),
DataValue::String(format!("Item {i}")),
DataValue::Float(i as f64 * 10.5),
]))
.unwrap();
}
Arc::new(DataView::new(Arc::new(table)))
}
#[test]
fn test_viewport_basic() {
let dataview = create_test_dataview();
let mut viewport = ViewportManager::new(dataview);
viewport.set_viewport(0, 0, 80, 24);
assert_eq!(viewport.viewport_rows(), 0..24);
assert_eq!(viewport.viewport_cols(), 0..3);
let visible_rows = viewport.get_visible_rows();
assert_eq!(visible_rows.len(), 24);
}
#[test]
fn test_column_width_calculation() {
let dataview = create_test_dataview();
let mut viewport = ViewportManager::new(dataview);
viewport.set_viewport(0, 0, 80, 10);
let widths = viewport.get_column_widths();
assert_eq!(widths.len(), 3);
assert!(widths[0] < 10);
assert!(widths[1] > widths[0]);
}
#[test]
fn test_viewport_scrolling() {
let dataview = create_test_dataview();
let mut viewport = ViewportManager::new(dataview);
viewport.set_viewport(0, 0, 80, 24);
viewport.scroll_by(10, 0);
assert_eq!(viewport.viewport_rows(), 10..34);
viewport.scroll_by(-5, 1);
assert_eq!(viewport.viewport_rows(), 5..29);
assert_eq!(viewport.viewport_cols(), 1..3);
}
#[test]
fn test_navigate_to_last_and_first_column() {
let dataview = create_test_dataview();
let mut vm = ViewportManager::new(dataview);
vm.update_terminal_size(120, 40);
let result = vm.navigate_to_last_column();
assert_eq!(vm.get_crosshair_col(), 2); assert_eq!(result.column_position, 2);
let result = vm.navigate_to_first_column();
assert_eq!(vm.get_crosshair_col(), 0);
assert_eq!(result.column_position, 0);
}
#[test]
fn test_column_reorder_right_with_crosshair() {
let dataview = create_test_dataview();
let mut vm = ViewportManager::new(dataview);
vm.update_terminal_size(120, 40);
vm.crosshair_col = 0;
let result = vm.reorder_column_right(0);
assert!(result.success);
assert_eq!(result.new_column_position, 1);
assert_eq!(vm.get_crosshair_col(), 1);
let headers = vm.dataview.column_names();
assert_eq!(headers[0], "name"); assert_eq!(headers[1], "id"); }
#[test]
fn test_column_reorder_left_with_crosshair() {
let dataview = create_test_dataview();
let mut vm = ViewportManager::new(dataview);
vm.update_terminal_size(120, 40);
vm.crosshair_col = 1;
let result = vm.reorder_column_left(1);
assert!(result.success);
assert_eq!(result.new_column_position, 0);
assert_eq!(vm.get_crosshair_col(), 0); }
#[test]
fn test_hide_column_adjusts_crosshair() {
let dataview = create_test_dataview();
let mut vm = ViewportManager::new(dataview);
vm.update_terminal_size(120, 40);
vm.crosshair_col = 1; let success = vm.hide_column(1);
assert!(success);
assert_eq!(vm.get_crosshair_col(), 1);
assert_eq!(vm.dataview.column_count(), 2);
vm.crosshair_col = 1; let success = vm.hide_column(1);
assert!(success);
assert_eq!(vm.get_crosshair_col(), 0); assert_eq!(vm.dataview.column_count(), 1); }
#[test]
fn test_goto_line_centers_viewport() {
let dataview = create_test_dataview();
let mut vm = ViewportManager::new(dataview);
vm.update_terminal_size(120, 40);
let result = vm.goto_line(50);
assert_eq!(result.row_position, 50);
assert_eq!(vm.get_crosshair_row(), 50);
let visible_rows = 34; let expected_offset = 50 - (visible_rows / 2);
assert_eq!(result.row_scroll_offset, expected_offset);
}
#[test]
fn test_page_navigation() {
let dataview = create_test_dataview();
let mut vm = ViewportManager::new(dataview);
vm.update_terminal_size(120, 40);
let initial_row = vm.get_crosshair_row();
let result = vm.page_down();
assert!(result.row_position > initial_row);
assert_eq!(vm.get_crosshair_row(), result.row_position);
vm.page_down(); vm.page_down();
let prev_position = vm.get_crosshair_row();
let result = vm.page_up();
assert!(result.row_position < prev_position); }
#[test]
fn test_cursor_lock_mode() {
let dataview = create_test_dataview();
let mut vm = ViewportManager::new(dataview);
vm.update_terminal_size(120, 40);
vm.toggle_cursor_lock();
assert!(vm.is_cursor_locked());
let initial_viewport_position = vm.get_crosshair_row() - vm.viewport_rows.start;
let result = vm.navigate_row_down();
if result.viewport_changed {
let new_viewport_position = vm.get_crosshair_row() - vm.viewport_rows.start;
assert_eq!(initial_viewport_position, new_viewport_position);
}
}
#[test]
fn test_viewport_lock_prevents_scrolling() {
let dataview = create_test_dataview();
let mut vm = ViewportManager::new(dataview);
vm.update_terminal_size(120, 40);
vm.toggle_viewport_lock();
assert!(vm.is_viewport_locked());
let initial_viewport = vm.viewport_rows.clone();
let result = vm.navigate_row_down();
assert_eq!(vm.viewport_rows, initial_viewport);
assert!(!result.viewport_changed);
}
#[test]
fn test_h_m_l_viewport_navigation() {
let dataview = create_test_dataview();
let mut vm = ViewportManager::new(dataview);
vm.update_terminal_size(120, 40);
for _ in 0..20 {
vm.navigate_row_down();
}
let result = vm.navigate_to_viewport_top();
assert_eq!(vm.get_crosshair_row(), vm.viewport_rows.start);
let result = vm.navigate_to_viewport_bottom();
assert_eq!(vm.get_crosshair_row(), vm.viewport_rows.end - 1);
let result = vm.navigate_to_viewport_middle();
let expected_middle =
vm.viewport_rows.start + (vm.viewport_rows.end - vm.viewport_rows.start) / 2;
assert_eq!(vm.get_crosshair_row(), expected_middle);
}
#[test]
fn test_out_of_order_column_navigation() {
let mut table = DataTable::new("test");
for i in 0..12 {
table.add_column(DataColumn::new(format!("col{i}")));
}
for row in 0..10 {
let mut values = Vec::new();
for col in 0..12 {
values.push(DataValue::String(format!("r{row}c{col}")));
}
table.add_row(DataRow::new(values)).unwrap();
}
let dataview =
DataView::new(Arc::new(table)).with_columns(vec![11, 0, 5, 3, 8, 1, 10, 2, 7, 4, 9, 6]);
let mut vm = ViewportManager::new(Arc::new(dataview));
vm.update_terminal_size(200, 40);
let column_names = vm.dataview.column_names();
assert_eq!(
column_names[0], "col11",
"First visual column should be col11"
);
assert_eq!(
column_names[1], "col0",
"Second visual column should be col0"
);
assert_eq!(
column_names[2], "col5",
"Third visual column should be col5"
);
vm.crosshair_col = 0;
let mut visual_positions = vec![0];
let mut datatable_positions = vec![];
let display_cols = vm.dataview.get_display_columns();
datatable_positions.push(display_cols[0]);
for i in 0..11 {
let current_visual = vm.get_crosshair_col();
let result = vm.navigate_column_right(current_visual);
let new_visual = vm.get_crosshair_col();
assert_eq!(
new_visual,
current_visual + 1,
"Crosshair should move from visual position {} to {}, but got {}",
current_visual,
current_visual + 1,
new_visual
);
visual_positions.push(new_visual);
let display_cols = vm.dataview.get_display_columns();
datatable_positions.push(display_cols[new_visual]);
}
assert_eq!(
visual_positions,
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
"Crosshair should move through visual positions sequentially"
);
assert_eq!(
datatable_positions,
vec![11, 0, 5, 3, 8, 1, 10, 2, 7, 4, 9, 6],
"DataTable indices should match our column selection order"
);
for _i in (0..11).rev() {
let current_visual = vm.get_crosshair_col();
let _result = vm.navigate_column_left(current_visual);
let new_visual = vm.get_crosshair_col();
assert_eq!(
new_visual,
current_visual - 1,
"Crosshair should move from visual position {} to {}, but got {}",
current_visual,
current_visual - 1,
new_visual
);
}
assert_eq!(
vm.get_crosshair_col(),
0,
"Should be back at first visual column"
);
vm.hide_column(2);
vm.crosshair_col = 0;
let _result1 = vm.navigate_column_right(0);
assert_eq!(vm.get_crosshair_col(), 1, "Should be at visual position 1");
let _result2 = vm.navigate_column_right(1);
assert_eq!(
vm.get_crosshair_col(),
2,
"Should be at visual position 2 after hiding"
);
let visible_cols = vm.dataview.column_names();
assert_eq!(
visible_cols[2], "col3",
"Column at position 2 should be col3 after hiding col5"
);
}
}