1use std::ops::Range;
15use std::sync::Arc;
16use tracing::debug;
17
18use crate::data::data_view::DataView;
19use crate::data::datatable::DataRow;
20use crate::ui::viewport::column_width_calculator::{
21 COLUMN_PADDING, DEFAULT_COL_WIDTH, MAX_COL_WIDTH, MAX_COL_WIDTH_DATA_FOCUS, MIN_COL_WIDTH,
22};
23use crate::ui::viewport::{ColumnPackingMode, ColumnWidthCalculator};
24
25#[derive(Debug, Clone)]
27pub struct NavigationResult {
28 pub column_position: usize,
30 pub scroll_offset: usize,
32 pub description: String,
34 pub viewport_changed: bool,
36}
37
38#[derive(Debug, Clone)]
40pub struct RowNavigationResult {
41 pub row_position: usize,
43 pub row_scroll_offset: usize,
45 pub description: String,
47 pub viewport_changed: bool,
49}
50
51#[derive(Debug, Clone)]
53pub struct ColumnReorderResult {
54 pub new_column_position: usize,
56 pub description: String,
58 pub success: bool,
60}
61
62#[derive(Debug, Clone)]
64pub struct ColumnOperationResult {
65 pub success: bool,
67 pub description: String,
69 pub updated_dataview: Option<DataView>,
71 pub new_column_position: Option<usize>,
73 pub new_viewport: Option<std::ops::Range<usize>>,
75 pub affected_count: Option<usize>,
77}
78
79impl ColumnOperationResult {
80 pub fn failure(description: impl Into<String>) -> Self {
82 Self {
83 success: false,
84 description: description.into(),
85 updated_dataview: None,
86 new_column_position: None,
87 new_viewport: None,
88 affected_count: None,
89 }
90 }
91
92 pub fn success(description: impl Into<String>) -> Self {
94 Self {
95 success: true,
96 description: description.into(),
97 updated_dataview: None,
98 new_column_position: None,
99 new_viewport: None,
100 affected_count: None,
101 }
102 }
103}
104
105const TABLE_CHROME_ROWS: usize = 3;
114
115const TABLE_BORDER_WIDTH: u16 = 4;
117
118pub struct ViewportManager {
120 dataview: Arc<DataView>,
122
123 viewport_rows: Range<usize>,
125 viewport_cols: Range<usize>,
126
127 terminal_width: u16,
129 terminal_height: u16,
130
131 width_calculator: ColumnWidthCalculator,
133
134 visible_row_cache: Vec<usize>,
136
137 cache_signature: u64,
139
140 cache_dirty: bool,
142
143 crosshair_row: usize,
146 crosshair_col: usize,
147
148 cursor_lock: bool,
150 cursor_lock_position: Option<usize>,
152
153 viewport_lock: bool,
155 viewport_lock_boundaries: Option<std::ops::Range<usize>>,
157}
158
159impl ViewportManager {
160 pub fn get_viewport_range(&self) -> std::ops::Range<usize> {
162 self.viewport_cols.clone()
163 }
164
165 pub fn get_viewport_rows(&self) -> std::ops::Range<usize> {
167 self.viewport_rows.clone()
168 }
169
170 pub fn set_crosshair(&mut self, row: usize, col: usize) {
172 self.crosshair_row = row;
173 self.crosshair_col = col;
174 debug!(target: "viewport_manager",
175 "Crosshair set to visual position: row={}, col={}", row, col);
176 }
177
178 pub fn set_crosshair_row(&mut self, row: usize) {
180 let total_rows = self.dataview.row_count();
181
182 let clamped_row = row.min(total_rows.saturating_sub(1));
184 self.crosshair_row = clamped_row;
185
186 if self.viewport_lock {
188 debug!(target: "viewport_manager",
189 "Crosshair row set to: {} (viewport locked, no scroll adjustment)",
190 clamped_row);
191 return;
192 }
193
194 let viewport_height = self.viewport_rows.len();
196 let mut viewport_changed = false;
197
198 if clamped_row < self.viewport_rows.start {
199 self.viewport_rows = clamped_row..(clamped_row + viewport_height).min(total_rows);
201 viewport_changed = true;
202 } else if clamped_row >= self.viewport_rows.end {
203 let new_start = clamped_row.saturating_sub(viewport_height.saturating_sub(1));
205 self.viewport_rows = new_start..(new_start + viewport_height).min(total_rows);
206 viewport_changed = true;
207 }
208
209 if viewport_changed {
210 debug!(target: "viewport_manager",
211 "Crosshair row set to: {}, adjusted viewport to: {:?}",
212 clamped_row, self.viewport_rows);
213 } else {
214 debug!(target: "viewport_manager",
215 "Crosshair row set to: {}", clamped_row);
216 }
217 }
218
219 pub fn set_crosshair_column(&mut self, col: usize) {
221 let total_columns = self.dataview.get_display_columns().len();
222
223 let clamped_col = col.min(total_columns.saturating_sub(1));
225 self.crosshair_col = clamped_col;
226
227 if self.viewport_lock {
229 debug!(target: "viewport_manager",
230 "Crosshair column set to: {} (viewport locked, no scroll adjustment)",
231 clamped_col);
232 return;
233 }
234
235 let terminal_width = self.terminal_width.saturating_sub(4); if self.set_current_column(clamped_col) {
238 debug!(target: "viewport_manager",
239 "Crosshair column set to: {} with viewport adjustment", clamped_col);
240 } else {
241 debug!(target: "viewport_manager",
242 "Crosshair column set to: {}", clamped_col);
243 }
244 }
245
246 pub fn get_crosshair_col(&self) -> usize {
248 self.crosshair_col
249 }
250
251 pub fn get_crosshair_row(&self) -> usize {
253 self.crosshair_row
254 }
255
256 pub fn get_selected_row(&self) -> usize {
258 self.crosshair_row
259 }
260
261 pub fn get_selected_column(&self) -> usize {
263 self.crosshair_col
264 }
265
266 pub fn get_crosshair_position(&self) -> (usize, usize) {
268 (self.crosshair_row, self.crosshair_col)
269 }
270
271 pub fn get_scroll_offset(&self) -> (usize, usize) {
273 (self.viewport_rows.start, self.viewport_cols.start)
274 }
275
276 pub fn set_scroll_offset(&mut self, row_offset: usize, col_offset: usize) {
278 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
279 let viewport_width = self.viewport_cols.end - self.viewport_cols.start;
280
281 self.viewport_rows = row_offset..(row_offset + viewport_height);
283 self.viewport_cols = col_offset..(col_offset + viewport_width);
284
285 if self.crosshair_row < self.viewport_rows.start {
287 self.crosshair_row = self.viewport_rows.start;
288 } else if self.crosshair_row >= self.viewport_rows.end {
289 self.crosshair_row = self.viewport_rows.end.saturating_sub(1);
290 }
291
292 if self.crosshair_col < self.viewport_cols.start {
293 self.crosshair_col = self.viewport_cols.start;
294 } else if self.crosshair_col >= self.viewport_cols.end {
295 self.crosshair_col = self.viewport_cols.end.saturating_sub(1);
296 }
297
298 self.cache_dirty = true;
299 }
300
301 pub fn get_crosshair_viewport_position(&self) -> Option<(usize, usize)> {
304 if self.crosshair_row < self.viewport_rows.start
307 || self.crosshair_row >= self.viewport_rows.end
308 {
309 return None;
310 }
311
312 let pinned_count = self.dataview.get_pinned_columns().len();
314
315 if self.crosshair_col < pinned_count {
317 return Some((
318 self.crosshair_row - self.viewport_rows.start,
319 self.crosshair_col, ));
321 }
322
323 let scrollable_col = self.crosshair_col - pinned_count;
326 if scrollable_col >= self.viewport_cols.start && scrollable_col < self.viewport_cols.end {
327 let visual_col_in_viewport = pinned_count + (scrollable_col - self.viewport_cols.start);
330 return Some((
331 self.crosshair_row - self.viewport_rows.start,
332 visual_col_in_viewport,
333 ));
334 }
335
336 None
337 }
338
339 pub fn navigate_row_up(&mut self) -> RowNavigationResult {
341 let total_rows = self.dataview.row_count();
342
343 if self.viewport_lock {
345 debug!(target: "viewport_manager",
346 "navigate_row_up: Viewport locked, crosshair={}, viewport={:?}",
347 self.crosshair_row, self.viewport_rows);
348 if self.crosshair_row > self.viewport_rows.start {
350 self.crosshair_row -= 1;
351 return RowNavigationResult {
352 row_position: self.crosshair_row,
353 row_scroll_offset: self.viewport_rows.start,
354 description: "Moved within locked viewport".to_string(),
355 viewport_changed: false,
356 };
357 } else {
358 return RowNavigationResult {
360 row_position: self.crosshair_row,
361 row_scroll_offset: self.viewport_rows.start,
362 description: "Moved within locked viewport".to_string(),
363 viewport_changed: false,
364 };
365 }
366 }
367
368 if self.cursor_lock {
370 if let Some(lock_position) = self.cursor_lock_position {
371 if self.viewport_rows.start == 0 {
373 return RowNavigationResult {
375 row_position: self.crosshair_row,
376 row_scroll_offset: self.viewport_rows.start,
377 description: "At top of data".to_string(),
378 viewport_changed: false,
379 };
380 }
381
382 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
383 let new_viewport_start = self.viewport_rows.start.saturating_sub(1);
384
385 self.viewport_rows =
387 new_viewport_start..(new_viewport_start + viewport_height).min(total_rows);
388
389 self.crosshair_row = (self.viewport_rows.start + lock_position)
391 .min(self.viewport_rows.end.saturating_sub(1));
392
393 return RowNavigationResult {
394 row_position: self.crosshair_row,
395 row_scroll_offset: self.viewport_rows.start,
396 description: format!(
397 "Scrolled up (locked at viewport row {})",
398 lock_position + 1
399 ),
400 viewport_changed: true,
401 };
402 }
403 }
404
405 if self.crosshair_row == 0 {
408 return RowNavigationResult {
410 row_position: 0,
411 row_scroll_offset: self.viewport_rows.start,
412 description: "Already at first row".to_string(),
413 viewport_changed: false,
414 };
415 }
416
417 let new_row = self.crosshair_row - 1;
418 self.crosshair_row = new_row;
419
420 let viewport_changed = if new_row < self.viewport_rows.start {
422 self.viewport_rows = new_row..self.viewport_rows.end.saturating_sub(1);
423 true
424 } else {
425 false
426 };
427
428 RowNavigationResult {
429 row_position: new_row,
430 row_scroll_offset: self.viewport_rows.start,
431 description: format!("Move to row {}", new_row + 1),
432 viewport_changed,
433 }
434 }
435
436 pub fn navigate_row_down(&mut self) -> RowNavigationResult {
438 let total_rows = self.dataview.row_count();
439
440 if self.viewport_lock {
442 debug!(target: "viewport_manager",
443 "navigate_row_down: Viewport locked, crosshair={}, viewport={:?}",
444 self.crosshair_row, self.viewport_rows);
445 if self.crosshair_row < self.viewport_rows.end - 1
447 && self.crosshair_row < total_rows - 1
448 {
449 self.crosshair_row += 1;
450 return RowNavigationResult {
451 row_position: self.crosshair_row,
452 row_scroll_offset: self.viewport_rows.start,
453 description: "Moved within locked viewport".to_string(),
454 viewport_changed: false,
455 };
456 } else {
457 return RowNavigationResult {
459 row_position: self.crosshair_row,
460 row_scroll_offset: self.viewport_rows.start,
461 description: "Moved within locked viewport".to_string(),
462 viewport_changed: false,
463 };
464 }
465 }
466
467 if self.cursor_lock {
469 if let Some(lock_position) = self.cursor_lock_position {
470 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
472 let new_viewport_start =
473 (self.viewport_rows.start + 1).min(total_rows.saturating_sub(viewport_height));
474
475 if new_viewport_start == self.viewport_rows.start {
476 return RowNavigationResult {
478 row_position: self.crosshair_row,
479 row_scroll_offset: self.viewport_rows.start,
480 description: "At bottom of data".to_string(),
481 viewport_changed: false,
482 };
483 }
484
485 self.viewport_rows =
487 new_viewport_start..(new_viewport_start + viewport_height).min(total_rows);
488
489 self.crosshair_row = (self.viewport_rows.start + lock_position)
491 .min(self.viewport_rows.end.saturating_sub(1));
492
493 return RowNavigationResult {
494 row_position: self.crosshair_row,
495 row_scroll_offset: self.viewport_rows.start,
496 description: format!(
497 "Scrolled down (locked at viewport row {})",
498 lock_position + 1
499 ),
500 viewport_changed: true,
501 };
502 }
503 }
504
505 if self.crosshair_row + 1 >= total_rows {
508 let last_row = total_rows.saturating_sub(1);
510 return RowNavigationResult {
511 row_position: last_row,
512 row_scroll_offset: self.viewport_rows.start,
513 description: "Already at last row".to_string(),
514 viewport_changed: false,
515 };
516 }
517
518 let new_row = self.crosshair_row + 1;
519 self.crosshair_row = new_row;
520
521 let viewport_changed = if new_row >= self.viewport_rows.end {
524 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
526 self.viewport_rows = (new_row + 1).saturating_sub(viewport_height)..(new_row + 1);
527 true
528 } else {
529 false
530 };
531
532 RowNavigationResult {
533 row_position: new_row,
534 row_scroll_offset: self.viewport_rows.start,
535 description: format!("Move to row {}", new_row + 1),
536 viewport_changed,
537 }
538 }
539
540 pub fn new(dataview: Arc<DataView>) -> Self {
542 let display_columns = dataview.get_display_columns();
544 let visible_col_count = display_columns.len();
545 let total_col_count = dataview.source().column_count(); let total_rows = dataview.row_count();
547
548 let initial_viewport_cols = if visible_col_count > 0 {
550 0..visible_col_count.min(20) } else {
552 0..0
553 };
554
555 let default_visible_rows = 50usize; let initial_viewport_rows = if total_rows > 0 {
559 0..total_rows.min(default_visible_rows)
560 } else {
561 0..0
562 };
563
564 Self {
565 dataview,
566 viewport_rows: initial_viewport_rows,
567 viewport_cols: initial_viewport_cols,
568 terminal_width: 80,
569 terminal_height: 24,
570 width_calculator: ColumnWidthCalculator::new(),
571 visible_row_cache: Vec::new(),
572 cache_signature: 0,
573 cache_dirty: true,
574 crosshair_row: 0,
575 crosshair_col: 0,
576 cursor_lock: false,
577 cursor_lock_position: None,
578 viewport_lock: false,
579 viewport_lock_boundaries: None,
580 }
581 }
582
583 pub fn set_dataview(&mut self, dataview: Arc<DataView>) {
585 self.dataview = dataview;
586 self.invalidate_cache();
587 }
588
589 pub fn reset_crosshair(&mut self) {
591 self.crosshair_row = 0;
592 self.crosshair_col = 0;
593 self.cursor_lock = false;
594 self.cursor_lock_position = None;
595 }
596
597 pub fn get_packing_mode(&self) -> ColumnPackingMode {
599 self.width_calculator.get_packing_mode()
600 }
601
602 pub fn set_packing_mode(&mut self, mode: ColumnPackingMode) {
604 self.width_calculator.set_packing_mode(mode);
605 self.invalidate_cache();
606 }
607
608 pub fn cycle_packing_mode(&mut self) -> ColumnPackingMode {
610 self.width_calculator.cycle_packing_mode();
611 self.invalidate_cache();
612 self.width_calculator.get_packing_mode()
613 }
614
615 pub fn set_viewport(&mut self, row_offset: usize, col_offset: usize, width: u16, height: u16) {
617 let new_rows = row_offset
618 ..row_offset
619 .saturating_add(height as usize)
620 .min(self.dataview.row_count());
621
622 let display_columns = self.dataview.get_display_columns();
625 let visual_column_count = display_columns.len();
626
627 let columns_that_fit = self.calculate_columns_that_fit(col_offset, width);
629 let new_cols = col_offset
630 ..col_offset
631 .saturating_add(columns_that_fit)
632 .min(visual_column_count);
633
634 if new_rows != self.viewport_rows || new_cols != self.viewport_cols {
636 self.viewport_rows = new_rows;
637 self.viewport_cols = new_cols;
638 self.terminal_width = width;
639 self.terminal_height = height;
640 self.cache_dirty = true;
641 }
642 }
643
644 pub fn update_terminal_size(&mut self, terminal_width: u16, terminal_height: u16) -> usize {
647 let visible_rows = (terminal_height as usize).max(10);
650
651 debug!(target: "viewport_manager",
652 "update_terminal_size: terminal_height={}, calculated visible_rows={}",
653 terminal_height, visible_rows
654 );
655
656 let old_viewport = self.viewport_rows.clone();
657
658 self.terminal_width = terminal_width;
660 self.terminal_height = terminal_height;
661
662 let total_rows = self.dataview.row_count();
665
666 let viewport_size = self.viewport_rows.end - self.viewport_rows.start;
668 if viewport_size != visible_rows && total_rows > 0 {
669 if self.crosshair_row < self.viewport_rows.start {
672 self.viewport_rows =
674 self.crosshair_row..(self.crosshair_row + visible_rows).min(total_rows);
675 } else if self.crosshair_row >= self.viewport_rows.start + visible_rows {
676 let start = self.crosshair_row.saturating_sub(visible_rows - 1);
678 self.viewport_rows = start..(start + visible_rows).min(total_rows);
679 } else {
680 self.viewport_rows = self.viewport_rows.start
682 ..(self.viewport_rows.start + visible_rows).min(total_rows);
683 }
684 }
685
686 let visible_column_count = self.dataview.get_display_columns().len();
689 if visible_column_count > 0 {
690 let columns_that_fit = self.calculate_columns_that_fit(
694 self.viewport_cols.start,
695 terminal_width.saturating_sub(2), );
697
698 let new_col_viewport_end = self
699 .viewport_cols
700 .start
701 .saturating_add(columns_that_fit)
702 .min(visible_column_count);
703
704 let old_col_viewport = self.viewport_cols.clone();
705 self.viewport_cols = self.viewport_cols.start..new_col_viewport_end;
706
707 if old_col_viewport != self.viewport_cols {
708 debug!(target: "viewport_manager",
709 "update_terminal_size - column viewport changed from {:?} to {:?}, terminal_width={}",
710 old_col_viewport, self.viewport_cols, terminal_width
711 );
712 self.cache_dirty = true;
713 }
714 }
715
716 if old_viewport != self.viewport_rows {
717 debug!(target: "navigation",
718 "ViewportManager::update_terminal_size - viewport changed from {:?} to {:?}, crosshair={}, visible_rows={}",
719 old_viewport, self.viewport_rows, self.crosshair_row, visible_rows
720 );
721 }
722
723 visible_rows
724 }
725
726 pub fn scroll_by(&mut self, row_delta: isize, col_delta: isize) {
728 let new_row_start = (self.viewport_rows.start as isize + row_delta).max(0) as usize;
729 let new_col_start = (self.viewport_cols.start as isize + col_delta).max(0) as usize;
730
731 self.set_viewport(
732 new_row_start,
733 new_col_start,
734 self.terminal_width,
735 self.terminal_height,
736 );
737 }
738
739 pub fn get_column_widths(&mut self) -> &[u16] {
741 self.width_calculator
742 .get_all_column_widths(&self.dataview, &self.viewport_rows)
743 }
744
745 pub fn get_column_width(&mut self, col_idx: usize) -> u16 {
747 self.width_calculator
748 .get_column_width(&self.dataview, &self.viewport_rows, col_idx)
749 }
750
751 pub fn get_visible_rows(&self) -> Vec<DataRow> {
753 let mut rows = Vec::with_capacity(self.viewport_rows.len());
754
755 for row_idx in self.viewport_rows.clone() {
756 if let Some(row) = self.dataview.get_row(row_idx) {
757 rows.push(row);
758 }
759 }
760
761 rows
762 }
763
764 pub fn get_visible_row(&self, viewport_row: usize) -> Option<DataRow> {
766 let absolute_row = self.viewport_rows.start + viewport_row;
767 if absolute_row < self.viewport_rows.end {
768 self.dataview.get_row(absolute_row)
769 } else {
770 None
771 }
772 }
773
774 pub fn get_visible_columns(&self) -> Vec<String> {
776 let display_column_names = self.dataview.get_display_column_names();
778
779 let mut visible = Vec::new();
781 for col_idx in self.viewport_cols.clone() {
782 if col_idx < display_column_names.len() {
783 visible.push(display_column_names[col_idx].clone());
784 }
785 }
786
787 visible
788 }
789
790 pub fn viewport_rows(&self) -> Range<usize> {
792 self.viewport_rows.clone()
793 }
794
795 pub fn viewport_cols(&self) -> Range<usize> {
797 self.viewport_cols.clone()
798 }
799
800 pub fn is_row_visible(&self, row_idx: usize) -> bool {
802 self.viewport_rows.contains(&row_idx)
803 }
804
805 pub fn is_column_visible(&self, col_idx: usize) -> bool {
807 self.viewport_cols.contains(&col_idx)
808 }
809
810 pub fn total_rows(&self) -> usize {
812 self.dataview.row_count()
813 }
814
815 pub fn total_columns(&self) -> usize {
817 self.dataview.column_count()
818 }
819
820 pub fn get_terminal_width(&self) -> u16 {
822 self.terminal_width
823 }
824
825 pub fn get_terminal_height(&self) -> usize {
827 self.terminal_height as usize
828 }
829
830 pub fn invalidate_cache(&mut self) {
832 self.cache_dirty = true;
833 self.width_calculator.mark_dirty();
834 }
835
836 pub fn calculate_visible_column_indices(&mut self, available_width: u16) -> Vec<usize> {
840 let display_columns = self.dataview.get_display_columns();
844 let total_visual_columns = display_columns.len();
845
846 if total_visual_columns == 0 {
847 return Vec::new();
848 }
849
850 let pinned_columns = self.dataview.get_pinned_columns();
852 let pinned_count = pinned_columns.len();
853
854 let mut used_width = 0u16;
855 let separator_width = 1u16;
856 let mut result = Vec::new();
857
858 tracing::debug!("[PIN_DEBUG] === calculate_visible_column_indices ===");
859 tracing::debug!(
860 "[PIN_DEBUG] available_width={}, total_visual_columns={}",
861 available_width,
862 total_visual_columns
863 );
864 tracing::debug!(
865 "[PIN_DEBUG] pinned_columns={:?} (count={})",
866 pinned_columns,
867 pinned_count
868 );
869 tracing::debug!("[PIN_DEBUG] viewport_cols={:?}", self.viewport_cols);
870 tracing::debug!("[PIN_DEBUG] display_columns={:?}", display_columns);
871
872 debug!(target: "viewport_manager",
873 "calculate_visible_column_indices: available_width={}, total_visual_columns={}, pinned_count={}, viewport_start={}",
874 available_width, total_visual_columns, pinned_count, self.viewport_cols.start);
875
876 for visual_idx in 0..pinned_count {
878 if visual_idx >= display_columns.len() {
879 break;
880 }
881
882 let datatable_idx = display_columns[visual_idx];
883 let width = self.width_calculator.get_column_width(
884 &self.dataview,
885 &self.viewport_rows,
886 datatable_idx,
887 );
888
889 used_width += width + separator_width;
891 result.push(datatable_idx);
892 tracing::debug!(
893 "[PIN_DEBUG] Added pinned column: visual_idx={}, datatable_idx={}, width={}",
894 visual_idx,
895 datatable_idx,
896 width
897 );
898 }
899
900 let scrollable_start = self.viewport_cols.start;
903 let visual_start = scrollable_start + pinned_count;
904
905 tracing::debug!(
906 "[PIN_DEBUG] viewport_cols.start={} is SCROLLABLE index",
907 self.viewport_cols.start
908 );
909 tracing::debug!(
910 "[PIN_DEBUG] visual_start={} (scrollable_start {} + pinned_count {})",
911 visual_start,
912 scrollable_start,
913 pinned_count
914 );
915
916 let visual_start = visual_start.min(total_visual_columns);
917
918 for visual_idx in visual_start..total_visual_columns {
920 let datatable_idx = display_columns[visual_idx];
922
923 let width = self.width_calculator.get_column_width(
924 &self.dataview,
925 &self.viewport_rows,
926 datatable_idx,
927 );
928
929 if used_width + width + separator_width <= available_width {
930 used_width += width + separator_width;
931 result.push(datatable_idx);
932 tracing::debug!("[PIN_DEBUG] Added scrollable column: visual_idx={}, datatable_idx={}, width={}", visual_idx, datatable_idx, width);
933 } else {
934 tracing::debug!(
935 "[PIN_DEBUG] Stopped at visual_idx={} - would exceed width",
936 visual_idx
937 );
938 break;
939 }
940 }
941
942 if result.is_empty() && total_visual_columns > 0 {
945 result.push(display_columns[0]);
946 }
947
948 tracing::debug!("[PIN_DEBUG] Final result: {:?}", result);
949 tracing::debug!("[PIN_DEBUG] === End calculate_visible_column_indices ===");
950 debug!(target: "viewport_manager",
951 "calculate_visible_column_indices RESULT: pinned={}, viewport_start={}, visual_start={} -> DataTable indices {:?}",
952 pinned_count, self.viewport_cols.start, visual_start, result);
953
954 result
955 }
956
957 pub fn calculate_columns_that_fit(&mut self, start_col: usize, available_width: u16) -> usize {
960 let mut used_width = 0u16;
963 let mut column_count = 0usize;
964 let separator_width = 1u16;
965
966 for col_idx in start_col..self.dataview.column_count() {
967 let width = self.width_calculator.get_column_width(
968 &self.dataview,
969 &self.viewport_rows,
970 col_idx,
971 );
972 if used_width + width + separator_width <= available_width {
973 used_width += width + separator_width;
974 column_count += 1;
975 } else {
976 break;
977 }
978 }
979
980 column_count.max(1) }
982
983 pub fn get_column_widths_for(&mut self, column_indices: &[usize]) -> Vec<u16> {
986 column_indices
987 .iter()
988 .map(|&idx| {
989 self.width_calculator
990 .get_column_width(&self.dataview, &self.viewport_rows, idx)
991 })
992 .collect()
993 }
994
995 pub fn update_column_viewport(&mut self, start_col: usize, available_width: u16) {
998 let col_count = self.calculate_columns_that_fit(start_col, available_width);
999 let end_col = (start_col + col_count).min(self.dataview.column_count());
1000
1001 if self.viewport_cols.start != start_col || self.viewport_cols.end != end_col {
1002 self.viewport_cols = start_col..end_col;
1003 self.cache_dirty = true;
1004 }
1005 }
1006
1007 pub fn dataview(&self) -> &DataView {
1009 &self.dataview
1010 }
1011
1012 pub fn clone_dataview(&self) -> DataView {
1015 (*self.dataview).clone()
1016 }
1017
1018 pub fn calculate_optimal_offset_for_last_column(&mut self, available_width: u16) -> usize {
1022 let display_columns = self.dataview.get_display_columns();
1026 if display_columns.is_empty() {
1027 return 0;
1028 }
1029
1030 let pinned = self.dataview.get_pinned_columns();
1031 let pinned_count = pinned.len();
1032
1033 let mut pinned_width = 0u16;
1035 let separator_width = 1u16;
1036 for &col_idx in pinned {
1037 let width = self.width_calculator.get_column_width(
1038 &self.dataview,
1039 &self.viewport_rows,
1040 col_idx,
1041 );
1042 pinned_width += width + separator_width;
1043 }
1044
1045 let available_for_scrollable = available_width.saturating_sub(pinned_width);
1047
1048 let scrollable_columns: Vec<usize> = display_columns
1050 .iter()
1051 .filter(|&&col| !pinned.contains(&col))
1052 .copied()
1053 .collect();
1054
1055 if scrollable_columns.is_empty() {
1056 return 0;
1057 }
1058
1059 let last_col_idx = *scrollable_columns.last().unwrap();
1061 let last_col_width = self.width_calculator.get_column_width(
1062 &self.dataview,
1063 &self.viewport_rows,
1064 last_col_idx,
1065 );
1066
1067 tracing::debug!(
1068 "Starting calculation: last_col_idx={}, width={}w, available={}w, scrollable_cols={}",
1069 last_col_idx,
1070 last_col_width,
1071 available_for_scrollable,
1072 scrollable_columns.len()
1073 );
1074
1075 let mut accumulated_width = last_col_width + separator_width;
1076 let mut best_offset = scrollable_columns.len() - 1; for (idx, &col_idx) in scrollable_columns.iter().enumerate().rev().skip(1) {
1080 let width = self.width_calculator.get_column_width(
1081 &self.dataview,
1082 &self.viewport_rows,
1083 col_idx,
1084 );
1085
1086 let width_with_separator = width + separator_width;
1087
1088 if accumulated_width + width_with_separator <= available_for_scrollable {
1089 accumulated_width += width_with_separator;
1091 best_offset = idx; tracing::trace!(
1093 "Column {} (idx {}) fits ({}w), accumulated={}w, new offset={}",
1094 col_idx,
1095 idx,
1096 width,
1097 accumulated_width,
1098 best_offset
1099 );
1100 } else {
1101 best_offset = idx + 1;
1104 tracing::trace!(
1105 "Column {} doesn't fit ({}w would make {}w total), stopping at offset {}",
1106 col_idx,
1107 width,
1108 accumulated_width + width_with_separator,
1109 best_offset
1110 );
1111 break;
1112 }
1113 }
1114
1115 let mut test_width = 0u16;
1121 let mut can_see_last = false;
1122 for idx in best_offset..scrollable_columns.len() {
1123 let col_idx = scrollable_columns[idx];
1124 let width = self.width_calculator.get_column_width(
1125 &self.dataview,
1126 &self.viewport_rows,
1127 col_idx,
1128 );
1129 test_width += width + separator_width;
1130
1131 if test_width > available_for_scrollable {
1132 tracing::warn!(
1135 "Offset {} doesn't show last column! Need {}w but have {}w",
1136 best_offset,
1137 test_width,
1138 available_for_scrollable
1139 );
1140 best_offset = best_offset + 1;
1142 can_see_last = false;
1143 break;
1144 }
1145 if idx == scrollable_columns.len() - 1 {
1146 can_see_last = true;
1147 }
1148 }
1149
1150 while !can_see_last && best_offset < scrollable_columns.len() {
1152 test_width = 0;
1153 for idx in best_offset..scrollable_columns.len() {
1154 let col_idx = scrollable_columns[idx];
1155 let width = self.width_calculator.get_column_width(
1156 &self.dataview,
1157 &self.viewport_rows,
1158 col_idx,
1159 );
1160 test_width += width + separator_width;
1161
1162 if test_width > available_for_scrollable {
1163 best_offset = best_offset + 1;
1164 break;
1165 }
1166 if idx == scrollable_columns.len() - 1 {
1167 can_see_last = true;
1168 }
1169 }
1170 }
1171
1172 tracing::debug!(
1174 "Final offset for last column: scrollable_offset={}, fits {} columns, last col width: {}w, verified last col visible: {}",
1175 best_offset,
1176 scrollable_columns.len() - best_offset,
1177 last_col_width,
1178 can_see_last
1179 );
1180
1181 best_offset
1182 }
1183
1184 pub fn debug_dump(&mut self, available_width: u16) -> String {
1186 let mut output = String::new();
1189 output.push_str("========== VIEWPORT MANAGER DEBUG ==========\n");
1190
1191 let total_cols = self.dataview.column_count();
1192 let pinned = self.dataview.get_pinned_columns();
1193 let pinned_count = pinned.len();
1194
1195 output.push_str(&format!("Total columns: {}\n", total_cols));
1196 output.push_str(&format!("Pinned columns: {:?}\n", pinned));
1197 output.push_str(&format!("Available width: {}w\n", available_width));
1198 output.push_str(&format!("Current viewport: {:?}\n", self.viewport_cols));
1199 output.push_str(&format!(
1200 "Packing mode: {} (Alt+S to cycle)\n",
1201 self.width_calculator.get_packing_mode().display_name()
1202 ));
1203 output.push_str("\n");
1204
1205 output.push_str("=== COLUMN WIDTH CALCULATIONS ===\n");
1207 output.push_str(&format!(
1208 "Mode: {}\n",
1209 self.width_calculator.get_packing_mode().display_name()
1210 ));
1211
1212 let debug_info = self.width_calculator.get_debug_info();
1214 if !debug_info.is_empty() {
1215 output.push_str("Visible columns in viewport:\n");
1216
1217 let mut visible_count = 0;
1219 for col_idx in self.viewport_cols.clone() {
1220 if col_idx < debug_info.len() {
1221 let (ref col_name, header_width, max_data_width, final_width, sample_count) =
1222 debug_info[col_idx];
1223
1224 let reason = match self.width_calculator.get_packing_mode() {
1226 ColumnPackingMode::DataFocus => {
1227 if max_data_width <= 3 {
1228 format!("Ultra aggressive (data:{}≤3 chars)", max_data_width)
1229 } else if max_data_width <= 10 && header_width > max_data_width * 2 {
1230 format!(
1231 "Aggressive truncate (data:{}≤10, header:{}>{} )",
1232 max_data_width,
1233 header_width,
1234 max_data_width * 2
1235 )
1236 } else if final_width == MAX_COL_WIDTH_DATA_FOCUS {
1237 "Max width reached".to_string()
1238 } else {
1239 "Data-based width".to_string()
1240 }
1241 }
1242 ColumnPackingMode::HeaderFocus => {
1243 if final_width == header_width + COLUMN_PADDING {
1244 "Full header shown".to_string()
1245 } else if final_width == MAX_COL_WIDTH {
1246 "Max width reached".to_string()
1247 } else {
1248 "Header priority".to_string()
1249 }
1250 }
1251 ColumnPackingMode::Balanced => {
1252 if header_width > max_data_width && final_width < header_width {
1253 "Header constrained by ratio".to_string()
1254 } else {
1255 "Balanced".to_string()
1256 }
1257 }
1258 };
1259
1260 output.push_str(&format!(
1261 " [{}] \"{}\":\n Header: {}w, Data: {}w → Final: {}w ({}, {} samples)\n",
1262 col_idx, col_name, header_width, max_data_width, final_width, reason, sample_count
1263 ));
1264
1265 visible_count += 1;
1266
1267 if visible_count >= 10 {
1269 let remaining = self.viewport_cols.end - self.viewport_cols.start - 10;
1270 if remaining > 0 {
1271 output.push_str(&format!(" ... and {} more columns\n", remaining));
1272 }
1273 break;
1274 }
1275 }
1276 }
1277 }
1278
1279 output.push_str("\n");
1280
1281 output.push_str("Column width summary (all columns):\n");
1283 let all_widths = self
1284 .width_calculator
1285 .get_all_column_widths(&self.dataview, &self.viewport_rows);
1286 for (idx, &width) in all_widths.iter().enumerate() {
1287 if idx >= 20 && idx < total_cols - 10 {
1288 if idx == 20 {
1289 output.push_str(" ... (showing only first 20 and last 10)\n");
1290 }
1291 continue;
1292 }
1293 output.push_str(&format!(" [{}] {}w\n", idx, width));
1294 }
1295 output.push_str("\n");
1296
1297 output.push_str("=== OPTIMAL OFFSET CALCULATION ===\n");
1299 let last_col_idx = total_cols - 1;
1300 let last_col_width = self.width_calculator.get_column_width(
1301 &self.dataview,
1302 &self.viewport_rows,
1303 last_col_idx,
1304 );
1305
1306 let separator_width = 1u16;
1308 let mut pinned_width = 0u16;
1309 for &col_idx in pinned {
1310 let width = self.width_calculator.get_column_width(
1311 &self.dataview,
1312 &self.viewport_rows,
1313 col_idx,
1314 );
1315 pinned_width += width + separator_width;
1316 }
1317 let available_for_scrollable = available_width.saturating_sub(pinned_width);
1318
1319 output.push_str(&format!(
1320 "Last column: {} (width: {}w)\n",
1321 last_col_idx, last_col_width
1322 ));
1323 output.push_str(&format!("Pinned width: {}w\n", pinned_width));
1324 output.push_str(&format!(
1325 "Available for scrollable: {}w\n",
1326 available_for_scrollable
1327 ));
1328 output.push_str("\n");
1329
1330 let mut accumulated_width = last_col_width + separator_width;
1332 let mut best_offset = last_col_idx;
1333
1334 output.push_str("Backtracking from last column:\n");
1335 output.push_str(&format!(
1336 " Start: column {} = {}w (accumulated: {}w)\n",
1337 last_col_idx, last_col_width, accumulated_width
1338 ));
1339
1340 for col_idx in (pinned_count..last_col_idx).rev() {
1341 let width = self.width_calculator.get_column_width(
1342 &self.dataview,
1343 &self.viewport_rows,
1344 col_idx,
1345 );
1346 let width_with_sep = width + separator_width;
1347
1348 if accumulated_width + width_with_sep <= available_for_scrollable {
1349 accumulated_width += width_with_sep;
1350 best_offset = col_idx;
1351 output.push_str(&format!(
1352 " Column {} fits: {}w (accumulated: {}w, offset: {})\n",
1353 col_idx, width, accumulated_width, best_offset
1354 ));
1355 } else {
1356 output.push_str(&format!(
1357 " Column {} doesn't fit: {}w (would make {}w > {}w)\n",
1358 col_idx,
1359 width,
1360 accumulated_width + width_with_sep,
1361 available_for_scrollable
1362 ));
1363 best_offset = col_idx + 1;
1364 break;
1365 }
1366 }
1367
1368 output.push_str(&format!(
1369 "\nCalculated offset: {} (absolute)\n",
1370 best_offset
1371 ));
1372
1373 output.push_str("\n=== VERIFICATION ===\n");
1375 let mut verify_width = 0u16;
1376 let mut can_show_last = true;
1377
1378 for test_idx in best_offset..=last_col_idx {
1379 let width = self.width_calculator.get_column_width(
1380 &self.dataview,
1381 &self.viewport_rows,
1382 test_idx,
1383 );
1384 verify_width += width + separator_width;
1385
1386 output.push_str(&format!(
1387 " Column {}: {}w (running total: {}w)\n",
1388 test_idx, width, verify_width
1389 ));
1390
1391 if verify_width > available_for_scrollable {
1392 output.push_str(&format!(
1393 " ❌ EXCEEDS LIMIT! {}w > {}w\n",
1394 verify_width, available_for_scrollable
1395 ));
1396 if test_idx == last_col_idx {
1397 can_show_last = false;
1398 output.push_str(" ❌ LAST COLUMN NOT VISIBLE!\n");
1399 }
1400 break;
1401 }
1402
1403 if test_idx == last_col_idx {
1404 output.push_str(" ✅ LAST COLUMN VISIBLE!\n");
1405 }
1406 }
1407
1408 output.push_str(&format!(
1409 "\nVerification result: last column visible = {}\n",
1410 can_show_last
1411 ));
1412
1413 output.push_str("\n=== CURRENT VIEWPORT RESULT ===\n");
1415 let visible_indices = self.calculate_visible_column_indices(available_width);
1416 output.push_str(&format!("Visible columns: {:?}\n", visible_indices));
1417 output.push_str(&format!(
1418 "Last visible column: {}\n",
1419 visible_indices.last().copied().unwrap_or(0)
1420 ));
1421 output.push_str(&format!(
1422 "Shows last column ({}): {}\n",
1423 last_col_idx,
1424 visible_indices.contains(&last_col_idx)
1425 ));
1426
1427 output.push_str("============================================\n");
1428 output
1429 }
1430
1431 pub fn get_column_names_ordered(&self) -> Vec<String> {
1434 self.dataview.column_names()
1435 }
1436
1437 pub fn get_visible_columns_info(
1440 &mut self,
1441 available_width: u16,
1442 ) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
1443 debug!(target: "viewport_manager",
1444 "get_visible_columns_info CALLED with width={}, current_viewport={:?}",
1445 available_width, self.viewport_cols);
1446
1447 let viewport_indices = self.calculate_visible_column_indices(available_width);
1449
1450 let display_order = self.dataview.get_display_columns();
1452 let mut visible_indices = Vec::new();
1453
1454 for &col_idx in &display_order {
1456 if viewport_indices.contains(&col_idx) {
1457 visible_indices.push(col_idx);
1458 }
1459 }
1460
1461 let pinned_columns = self.dataview.get_pinned_columns();
1463
1464 let mut pinned_visible = Vec::new();
1466 let mut scrollable_visible = Vec::new();
1467
1468 for &idx in &visible_indices {
1469 if pinned_columns.contains(&idx) {
1470 pinned_visible.push(idx);
1471 } else {
1472 scrollable_visible.push(idx);
1473 }
1474 }
1475
1476 debug!(target: "viewport_manager",
1477 "get_visible_columns_info: viewport={:?} -> ordered={:?} ({} pinned, {} scrollable)",
1478 viewport_indices, visible_indices, pinned_visible.len(), scrollable_visible.len());
1479
1480 debug!(target: "viewport_manager",
1481 "RENDERER DEBUG: viewport_indices={:?}, display_order={:?}, visible_indices={:?}",
1482 viewport_indices, display_order, visible_indices);
1483
1484 (visible_indices, pinned_visible, scrollable_visible)
1485 }
1486
1487 pub fn calculate_column_x_positions(&mut self, available_width: u16) -> (Vec<usize>, Vec<u16>) {
1490 let visible_indices = self.calculate_visible_column_indices(available_width);
1491 let mut x_positions = Vec::new();
1492 let mut current_x = 0u16;
1493 let separator_width = 1u16;
1494
1495 for &col_idx in &visible_indices {
1496 x_positions.push(current_x);
1497 let width = self.width_calculator.get_column_width(
1498 &self.dataview,
1499 &self.viewport_rows,
1500 col_idx,
1501 );
1502 current_x += width + separator_width;
1503 }
1504
1505 (visible_indices, x_positions)
1506 }
1507
1508 pub fn get_column_x_position(&mut self, column: usize, available_width: u16) -> Option<u16> {
1510 let (indices, positions) = self.calculate_column_x_positions(available_width);
1511 indices
1512 .iter()
1513 .position(|&idx| idx == column)
1514 .and_then(|pos| positions.get(pos).copied())
1515 }
1516
1517 pub fn calculate_visible_column_indices_ordered(&mut self, available_width: u16) -> Vec<usize> {
1519 let ordered_columns = self.dataview.get_display_columns();
1523 let mut visible_indices = Vec::new();
1524 let mut used_width = 0u16;
1525 let separator_width = 1u16;
1526
1527 tracing::trace!(
1528 "ViewportManager: Starting ordered column layout. Available width: {}w, DataView order: {:?}",
1529 available_width,
1530 ordered_columns
1531 );
1532
1533 for &col_idx in &ordered_columns {
1535 let width = self.width_calculator.get_column_width(
1536 &self.dataview,
1537 &self.viewport_rows,
1538 col_idx,
1539 );
1540
1541 if used_width + width + separator_width <= available_width {
1542 visible_indices.push(col_idx);
1543 used_width += width + separator_width;
1544 tracing::trace!(
1545 "Added column {} in DataView order: {}w (total used: {}w)",
1546 col_idx,
1547 width,
1548 used_width
1549 );
1550 } else {
1551 tracing::trace!(
1552 "Skipped column {} ({}w) - would exceed available width",
1553 col_idx,
1554 width
1555 );
1556 break; }
1558 }
1559
1560 tracing::trace!(
1561 "Final ordered layout: {} columns visible {:?}, {}w used of {}w",
1562 visible_indices.len(),
1563 visible_indices,
1564 used_width,
1565 available_width
1566 );
1567
1568 visible_indices
1569 }
1570
1571 pub fn get_display_position_for_datatable_column(
1574 &mut self,
1575 datatable_column: usize,
1576 available_width: u16,
1577 ) -> Option<usize> {
1578 let visible_columns_info = self.get_visible_columns_info(available_width);
1579 let visible_indices = visible_columns_info.0;
1580
1581 let position = visible_indices
1583 .iter()
1584 .position(|&col| col == datatable_column);
1585
1586 debug!(target: "viewport_manager",
1587 "get_display_position_for_datatable_column: datatable_column={}, visible_indices={:?}, position={:?}",
1588 datatable_column, visible_indices, position);
1589
1590 position
1591 }
1592
1593 pub fn get_crosshair_column(
1598 &mut self,
1599 current_datatable_column: usize,
1600 available_width: u16,
1601 ) -> Option<usize> {
1602 let visible_columns_info = self.get_visible_columns_info(available_width);
1604 let visible_indices = visible_columns_info.0;
1605
1606 let position = visible_indices
1608 .iter()
1609 .position(|&col| col == current_datatable_column);
1610
1611 debug!(target: "viewport_manager",
1612 "CROSSHAIR: current_datatable_column={}, visible_indices={:?}, crosshair_position={:?}",
1613 current_datatable_column, visible_indices, position);
1614
1615 position
1616 }
1617
1618 pub fn get_visual_display(
1622 &mut self,
1623 available_width: u16,
1624 _row_indices: &[usize], ) -> (Vec<String>, Vec<Vec<String>>, Vec<u16>) {
1626 let row_indices: Vec<usize> = (self.viewport_rows.start..self.viewport_rows.end).collect();
1628
1629 debug!(target: "viewport_manager",
1630 "get_visual_display: Using viewport_rows {:?} -> row_indices: {:?} (first 5)",
1631 self.viewport_rows,
1632 row_indices.iter().take(5).collect::<Vec<_>>());
1633 let visible_column_indices = self.calculate_visible_column_indices(available_width);
1636
1637 tracing::debug!(
1638 "[RENDER_DEBUG] visible_column_indices from calculate: {:?}",
1639 visible_column_indices
1640 );
1641
1642 let all_headers = self.dataview.get_display_column_names();
1644 let display_columns = self.dataview.get_display_columns();
1645 let total_visual_columns = all_headers.len();
1646
1647 debug!(target: "viewport_manager",
1648 "get_visual_display: {} total visual columns, viewport: {:?}",
1649 total_visual_columns, self.viewport_cols);
1650
1651 let headers: Vec<String> = visible_column_indices
1653 .iter()
1654 .filter_map(|&dt_idx| {
1655 display_columns
1657 .iter()
1658 .position(|&x| x == dt_idx)
1659 .and_then(|visual_idx| all_headers.get(visual_idx).cloned())
1660 })
1661 .collect();
1662
1663 tracing::debug!("[RENDER_DEBUG] headers: {:?}", headers);
1664
1665 let visual_rows: Vec<Vec<String>> = row_indices
1668 .iter()
1669 .filter_map(|&display_row_idx| {
1670 let row_data = self.dataview.get_row_visual_values(display_row_idx);
1673 if let Some(ref full_row) = row_data {
1674 if display_row_idx < 5 || display_row_idx >= 19900 {
1676 debug!(target: "viewport_manager",
1677 "DATAVIEW FETCH: display_row_idx {} -> data: {:?} (first 3 cols)",
1678 display_row_idx,
1679 full_row.iter().take(3).collect::<Vec<_>>());
1680 }
1681 }
1682 row_data.map(|full_row| {
1683 visible_column_indices
1685 .iter()
1686 .filter_map(|&dt_idx| {
1687 display_columns
1689 .iter()
1690 .position(|&x| x == dt_idx)
1691 .and_then(|visual_idx| full_row.get(visual_idx).cloned())
1692 })
1693 .collect()
1694 })
1695 })
1696 .collect();
1697
1698 let widths: Vec<u16> = visible_column_indices
1700 .iter()
1701 .map(|&dt_idx| {
1702 Some(self.width_calculator.get_column_width(
1703 &self.dataview,
1704 &self.viewport_rows,
1705 dt_idx,
1706 ))
1707 .unwrap_or(DEFAULT_COL_WIDTH)
1708 })
1709 .collect();
1710
1711 debug!(target: "viewport_manager",
1712 "get_visual_display RESULT: {} headers, {} rows",
1713 headers.len(), visual_rows.len());
1714 if let Some(first_row) = visual_rows.first() {
1715 debug!(target: "viewport_manager",
1716 "Alignment check (FIRST ROW): {:?}",
1717 headers.iter().zip(first_row).take(5)
1718 .map(|(h, v)| format!("{}: {}", h, v)).collect::<Vec<_>>());
1719 }
1720 if let Some(last_row) = visual_rows.last() {
1721 debug!(target: "viewport_manager",
1722 "Alignment check (LAST ROW): {:?}",
1723 headers.iter().zip(last_row).take(5)
1724 .map(|(h, v)| format!("{}: {}", h, v)).collect::<Vec<_>>());
1725 }
1726
1727 (headers, visual_rows, widths)
1728 }
1729
1730 pub fn get_visible_column_headers(&self, visible_indices: &[usize]) -> Vec<String> {
1733 let mut headers = Vec::new();
1734
1735 let source = self.dataview.source();
1738 let all_column_names = source.column_names();
1739
1740 for &visual_idx in visible_indices {
1741 if visual_idx < all_column_names.len() {
1742 headers.push(all_column_names[visual_idx].clone());
1743 } else {
1744 headers.push(format!("Column_{}", visual_idx));
1746 }
1747 }
1748
1749 debug!(target: "viewport_manager",
1750 "get_visible_column_headers: indices={:?} -> headers={:?}",
1751 visible_indices, headers);
1752
1753 headers
1754 }
1755
1756 pub fn get_crosshair_column_for_display(
1759 &mut self,
1760 current_display_position: usize,
1761 available_width: u16,
1762 ) -> Option<usize> {
1763 let display_columns = self.dataview.get_display_columns();
1765
1766 if current_display_position >= display_columns.len() {
1768 debug!(target: "viewport_manager",
1769 "CROSSHAIR DISPLAY: display_position {} out of bounds (max {})",
1770 current_display_position, display_columns.len());
1771 return None;
1772 }
1773
1774 let datatable_column = display_columns[current_display_position];
1776
1777 let visible_columns_info = self.get_visible_columns_info(available_width);
1779 let visible_indices = visible_columns_info.0;
1780
1781 let position = visible_indices
1783 .iter()
1784 .position(|&col| col == datatable_column);
1785
1786 debug!(target: "viewport_manager",
1787 "CROSSHAIR DISPLAY: display_pos={} -> datatable_col={} -> visible_indices={:?} -> crosshair_pos={:?}",
1788 current_display_position, datatable_column, visible_indices, position);
1789
1790 position
1791 }
1792
1793 pub fn calculate_efficiency_metrics(&mut self, available_width: u16) -> ViewportEfficiency {
1795 let visible_indices = self.calculate_visible_column_indices(available_width);
1797
1798 let mut used_width = 0u16;
1800 let separator_width = 1u16;
1801
1802 for &col_idx in &visible_indices {
1803 let width = self.width_calculator.get_column_width(
1804 &self.dataview,
1805 &self.viewport_rows,
1806 col_idx,
1807 );
1808 used_width += width + separator_width;
1809 }
1810
1811 if !visible_indices.is_empty() {
1813 used_width = used_width.saturating_sub(separator_width);
1814 }
1815
1816 let wasted_space = available_width.saturating_sub(used_width);
1817
1818 let next_column_width = if !visible_indices.is_empty() {
1820 let last_visible = *visible_indices.last().unwrap();
1821 if last_visible + 1 < self.dataview.column_count() {
1822 Some(self.width_calculator.get_column_width(
1823 &self.dataview,
1824 &self.viewport_rows,
1825 last_visible + 1,
1826 ))
1827 } else {
1828 None
1829 }
1830 } else {
1831 None
1832 };
1833
1834 let mut columns_that_could_fit = Vec::new();
1836 if wasted_space > MIN_COL_WIDTH + separator_width {
1837 let all_widths = self
1838 .width_calculator
1839 .get_all_column_widths(&self.dataview, &self.viewport_rows);
1840 for (idx, &width) in all_widths.iter().enumerate() {
1841 if !visible_indices.contains(&idx) {
1843 if width + separator_width <= wasted_space {
1844 columns_that_could_fit.push((idx, width));
1845 }
1846 }
1847 }
1848 }
1849
1850 let efficiency_percent = if available_width > 0 {
1851 ((used_width as f32 / available_width as f32) * 100.0) as u8
1852 } else {
1853 0
1854 };
1855
1856 ViewportEfficiency {
1857 available_width,
1858 used_width,
1859 wasted_space,
1860 efficiency_percent,
1861 visible_columns: visible_indices.len(),
1862 column_widths: visible_indices
1863 .iter()
1864 .map(|&idx| {
1865 Some(self.width_calculator.get_column_width(
1866 &self.dataview,
1867 &self.viewport_rows,
1868 idx,
1869 ))
1870 .unwrap_or(DEFAULT_COL_WIDTH)
1871 })
1872 .collect(),
1873 next_column_width,
1874 columns_that_could_fit,
1875 }
1876 }
1877
1878 pub fn navigate_to_first_column(&mut self) -> NavigationResult {
1881 if self.viewport_lock {
1883 self.crosshair_col = self.viewport_cols.start;
1885 return NavigationResult {
1886 column_position: self.crosshair_col,
1887 scroll_offset: self.viewport_cols.start,
1888 description: "Moved to first visible column (viewport locked)".to_string(),
1889 viewport_changed: false,
1890 };
1891 }
1892 let pinned_count = self.dataview.get_pinned_columns().len();
1894 let pinned_names = self.dataview.get_pinned_column_names();
1895
1896 let first_scrollable_column = pinned_count;
1898
1899 let new_scroll_offset = 0;
1901 let old_scroll_offset = self.viewport_cols.start;
1902
1903 let visible_indices = self
1905 .calculate_visible_column_indices_with_offset(self.terminal_width, new_scroll_offset);
1906 let viewport_end = if let Some(&last_idx) = visible_indices.last() {
1907 last_idx + 1
1908 } else {
1909 new_scroll_offset + 1
1910 };
1911
1912 self.viewport_cols = new_scroll_offset..viewport_end;
1914
1915 self.crosshair_col = first_scrollable_column;
1917
1918 let description = if pinned_count > 0 {
1920 format!(
1921 "First scrollable column selected (after {} pinned: {:?})",
1922 pinned_count, pinned_names
1923 )
1924 } else {
1925 "First column selected".to_string()
1926 };
1927
1928 let viewport_changed = old_scroll_offset != new_scroll_offset;
1929
1930 debug!(target: "viewport_manager",
1931 "navigate_to_first_column: pinned={}, first_scrollable={}, crosshair_col={}, scroll_offset={}->{}",
1932 pinned_count, first_scrollable_column, self.crosshair_col, old_scroll_offset, new_scroll_offset);
1933
1934 NavigationResult {
1935 column_position: first_scrollable_column,
1936 scroll_offset: new_scroll_offset,
1937 description,
1938 viewport_changed,
1939 }
1940 }
1941
1942 pub fn navigate_to_last_column(&mut self) -> NavigationResult {
1945 if self.viewport_lock {
1947 self.crosshair_col = self.viewport_cols.end.saturating_sub(1);
1949 return NavigationResult {
1950 column_position: self.crosshair_col,
1951 scroll_offset: self.viewport_cols.start,
1952 description: "Moved to last visible column (viewport locked)".to_string(),
1953 viewport_changed: false,
1954 };
1955 }
1956 let display_columns = self.dataview.get_display_columns();
1958 let total_visual_columns = display_columns.len();
1959
1960 if total_visual_columns == 0 {
1961 return NavigationResult {
1962 column_position: 0,
1963 scroll_offset: 0,
1964 description: "No columns available".to_string(),
1965 viewport_changed: false,
1966 };
1967 }
1968
1969 let last_visual_column = total_visual_columns - 1;
1971
1972 self.crosshair_col = last_visual_column;
1974
1975 let available_width = self.terminal_width;
1978 let pinned_count = self.dataview.get_pinned_columns().len();
1979
1980 let mut pinned_width = 0u16;
1982 for i in 0..pinned_count {
1983 let col_idx = display_columns[i];
1984 let width = self.width_calculator.get_column_width(
1985 &self.dataview,
1986 &self.viewport_rows,
1987 col_idx,
1988 );
1989 pinned_width += width + 3; }
1991
1992 let available_for_scrollable = available_width.saturating_sub(pinned_width);
1993
1994 let mut accumulated_width = 0u16;
1996 let mut new_scroll_offset = last_visual_column;
1997
1998 for visual_idx in (pinned_count..=last_visual_column).rev() {
2000 let col_idx = display_columns[visual_idx];
2001 let width = self.width_calculator.get_column_width(
2002 &self.dataview,
2003 &self.viewport_rows,
2004 col_idx,
2005 );
2006 accumulated_width += width + 3; if accumulated_width > available_for_scrollable {
2009 new_scroll_offset = visual_idx + 1;
2011 break;
2012 }
2013 new_scroll_offset = visual_idx;
2014 }
2015
2016 new_scroll_offset = new_scroll_offset.max(pinned_count);
2018
2019 let old_scroll_offset = self.viewport_cols.start;
2020 let viewport_changed = old_scroll_offset != new_scroll_offset;
2021
2022 let visible_indices = self
2024 .calculate_visible_column_indices_with_offset(self.terminal_width, new_scroll_offset);
2025 let viewport_end = if let Some(&last_idx) = visible_indices.last() {
2026 last_idx + 1
2027 } else {
2028 new_scroll_offset + 1
2029 };
2030
2031 self.viewport_cols = new_scroll_offset..viewport_end;
2033
2034 debug!(target: "viewport_manager",
2035 "navigate_to_last_column: last_visual={}, scroll_offset={}->{}",
2036 last_visual_column, old_scroll_offset, new_scroll_offset);
2037
2038 NavigationResult {
2039 column_position: last_visual_column,
2040 scroll_offset: new_scroll_offset,
2041 description: format!("Last column selected (column {})", last_visual_column + 1),
2042 viewport_changed,
2043 }
2044 }
2045
2046 pub fn navigate_column_left(&mut self, current_display_position: usize) -> NavigationResult {
2050 if self.viewport_lock {
2052 debug!(target: "viewport_manager",
2053 "navigate_column_left: Viewport locked, crosshair_col={}, viewport={:?}",
2054 self.crosshair_col, self.viewport_cols);
2055
2056 if self.crosshair_col > self.viewport_cols.start {
2058 self.crosshair_col -= 1;
2059 return NavigationResult {
2060 column_position: self.crosshair_col,
2061 scroll_offset: self.viewport_cols.start,
2062 description: "Moved within locked viewport".to_string(),
2063 viewport_changed: false,
2064 };
2065 } else {
2066 return NavigationResult {
2068 column_position: self.crosshair_col,
2069 scroll_offset: self.viewport_cols.start,
2070 description: "At left edge of locked viewport".to_string(),
2071 viewport_changed: false,
2072 };
2073 }
2074 }
2075
2076 let display_columns = self.dataview.get_display_columns();
2078 let total_display_columns = display_columns.len();
2079
2080 debug!(target: "viewport_manager",
2081 "navigate_column_left: current_display_pos={}, total_display={}, display_order={:?}",
2082 current_display_position, total_display_columns, display_columns);
2083
2084 let current_display_index = if current_display_position < total_display_columns {
2086 current_display_position
2087 } else {
2088 0 };
2090
2091 debug!(target: "viewport_manager",
2092 "navigate_column_left: using display_index={}",
2093 current_display_index);
2094
2095 if current_display_index == 0 {
2098 return NavigationResult {
2101 column_position: 0, scroll_offset: self.viewport_cols.start,
2103 description: "Already at first column".to_string(),
2104 viewport_changed: false,
2105 };
2106 }
2107
2108 let new_display_index = current_display_index - 1;
2109
2110 let new_visual_column = display_columns
2112 .get(new_display_index)
2113 .copied()
2114 .unwrap_or_else(|| {
2115 display_columns
2116 .get(current_display_index)
2117 .copied()
2118 .unwrap_or(0)
2119 });
2120
2121 let old_scroll_offset = self.viewport_cols.start;
2122
2123 debug!(target: "viewport_manager",
2127 "navigate_column_left: moving to datatable_column={}, current viewport={:?}",
2128 new_visual_column, self.viewport_cols);
2129
2130 let viewport_changed = self.set_current_column(new_display_index);
2132
2133 let column_names = self.dataview.column_names();
2136 let column_name = display_columns
2137 .get(new_display_index)
2138 .and_then(|&dt_idx| column_names.get(dt_idx))
2139 .map(|s| s.as_str())
2140 .unwrap_or("unknown");
2141 let description = format!(
2142 "Navigate left to column '{}' ({})",
2143 column_name,
2144 new_display_index + 1
2145 );
2146
2147 debug!(target: "viewport_manager",
2148 "navigate_column_left: display_pos {}→{}, datatable_col: {}, scroll: {}→{}, viewport_changed={}",
2149 current_display_index, new_display_index, new_visual_column,
2150 old_scroll_offset, self.viewport_cols.start, viewport_changed);
2151
2152 NavigationResult {
2153 column_position: new_display_index, scroll_offset: self.viewport_cols.start,
2155 description,
2156 viewport_changed,
2157 }
2158 }
2159
2160 pub fn navigate_column_right(&mut self, current_display_position: usize) -> NavigationResult {
2163 debug!(target: "viewport_manager",
2164 "=== CRITICAL DEBUG: navigate_column_right CALLED ===");
2165 debug!(target: "viewport_manager",
2166 "Input current_display_position: {}", current_display_position);
2167 debug!(target: "viewport_manager",
2168 "Current crosshair_col: {}", self.crosshair_col);
2169 debug!(target: "viewport_manager",
2170 "Current viewport_cols: {:?}", self.viewport_cols);
2171 if self.viewport_lock {
2173 debug!(target: "viewport_manager",
2174 "navigate_column_right: Viewport locked, crosshair_col={}, viewport={:?}",
2175 self.crosshair_col, self.viewport_cols);
2176
2177 if self.crosshair_col < self.viewport_cols.end - 1 {
2179 self.crosshair_col += 1;
2180 return NavigationResult {
2181 column_position: self.crosshair_col,
2182 scroll_offset: self.viewport_cols.start,
2183 description: "Moved within locked viewport".to_string(),
2184 viewport_changed: false,
2185 };
2186 } else {
2187 return NavigationResult {
2189 column_position: self.crosshair_col,
2190 scroll_offset: self.viewport_cols.start,
2191 description: "At right edge of locked viewport".to_string(),
2192 viewport_changed: false,
2193 };
2194 }
2195 }
2196
2197 let display_columns = self.dataview.get_display_columns();
2198 let total_display_columns = display_columns.len();
2199 let column_names = self.dataview.column_names();
2200
2201 debug!(target: "viewport_manager",
2203 "=== navigate_column_right DETAILED DEBUG ===");
2204 debug!(target: "viewport_manager",
2205 "ENTRY: current_display_pos={}, total_display_columns={}",
2206 current_display_position, total_display_columns);
2207 debug!(target: "viewport_manager",
2208 "display_columns (DataTable indices): {:?}", display_columns);
2209
2210 if current_display_position < display_columns.len() {
2212 let current_dt_idx = display_columns[current_display_position];
2213 let current_name = column_names
2214 .get(current_dt_idx)
2215 .map(|s| s.as_str())
2216 .unwrap_or("unknown");
2217 debug!(target: "viewport_manager",
2218 "Current position {} -> column '{}' (dt_idx={})",
2219 current_display_position, current_name, current_dt_idx);
2220 }
2221
2222 if current_display_position + 1 < display_columns.len() {
2223 let next_dt_idx = display_columns[current_display_position + 1];
2224 let next_name = column_names
2225 .get(next_dt_idx)
2226 .map(|s| s.as_str())
2227 .unwrap_or("unknown");
2228 debug!(target: "viewport_manager",
2229 "Next position {} -> column '{}' (dt_idx={})",
2230 current_display_position + 1, next_name, next_dt_idx);
2231 }
2232
2233 let current_display_index = if current_display_position < total_display_columns {
2235 current_display_position
2236 } else {
2237 debug!(target: "viewport_manager",
2238 "WARNING: current_display_position {} >= total_display_columns {}, resetting to 0",
2239 current_display_position, total_display_columns);
2240 0 };
2242
2243 debug!(target: "viewport_manager",
2244 "Validated: current_display_index={}",
2245 current_display_index);
2246
2247 if current_display_index + 1 >= total_display_columns {
2250 let last_display_index = total_display_columns.saturating_sub(1);
2252 debug!(target: "viewport_manager",
2253 "At last column boundary: current={}, total={}, returning last_display_index={}",
2254 current_display_index, total_display_columns, last_display_index);
2255 return NavigationResult {
2256 column_position: last_display_index, scroll_offset: self.viewport_cols.start,
2258 description: "Already at last column".to_string(),
2259 viewport_changed: false,
2260 };
2261 }
2262
2263 let new_display_index = current_display_index + 1;
2264
2265 let new_visual_column = display_columns
2267 .get(new_display_index)
2268 .copied()
2269 .unwrap_or_else(|| {
2270 tracing::error!(
2272 "[NAV_ERROR] Failed to get display column at index {}, total={}",
2273 new_display_index,
2274 display_columns.len()
2275 );
2276 display_columns
2278 .get(current_display_index)
2279 .copied()
2280 .unwrap_or(0)
2281 });
2282
2283 debug!(target: "viewport_manager",
2284 "navigate_column_right: display_pos {}→{}, new_visual_column={}",
2285 current_display_index, new_display_index, new_visual_column);
2286
2287 let old_scroll_offset = self.viewport_cols.start;
2288
2289 debug!(target: "viewport_manager",
2295 "navigate_column_right: moving to datatable_column={}, current viewport={:?}",
2296 new_visual_column, self.viewport_cols);
2297
2298 debug!(target: "viewport_manager",
2301 "navigate_column_right: before set_current_column(visual_idx={}), viewport={:?}",
2302 new_display_index, self.viewport_cols);
2303 let viewport_changed = self.set_current_column(new_display_index);
2304 debug!(target: "viewport_manager",
2305 "navigate_column_right: after set_current_column(visual_idx={}), viewport={:?}, changed={}",
2306 new_display_index, self.viewport_cols, viewport_changed);
2307
2308 let column_names = self.dataview.column_names();
2311 let column_name = display_columns
2312 .get(new_display_index)
2313 .and_then(|&dt_idx| column_names.get(dt_idx))
2314 .map(|s| s.as_str())
2315 .unwrap_or("unknown");
2316 let description = format!(
2317 "Navigate right to column '{}' ({})",
2318 column_name,
2319 new_display_index + 1
2320 );
2321
2322 debug!(target: "viewport_manager",
2324 "=== navigate_column_right RESULT ===");
2325 debug!(target: "viewport_manager",
2326 "Returning: column_position={} (visual/display index)", new_display_index);
2327 debug!(target: "viewport_manager",
2328 "Movement: {} -> {} (visual indices)", current_display_index, new_display_index);
2329 debug!(target: "viewport_manager",
2330 "Viewport: {:?}, changed={}", self.viewport_cols, viewport_changed);
2331 debug!(target: "viewport_manager",
2332 "Description: {}", description);
2333
2334 tracing::debug!("[NAV_DEBUG] Final result: column_position={} (visual/display idx), viewport_changed={}",
2335 new_display_index, viewport_changed);
2336 debug!(target: "viewport_manager",
2337 "navigate_column_right EXIT: display_pos {}→{}, datatable_col: {}, viewport: {:?}, scroll: {}→{}, viewport_changed={}",
2338 current_display_index, new_display_index, new_visual_column,
2339 self.viewport_cols, old_scroll_offset, self.viewport_cols.start, viewport_changed);
2340
2341 NavigationResult {
2342 column_position: new_display_index, scroll_offset: self.viewport_cols.start,
2344 description,
2345 viewport_changed,
2346 }
2347 }
2348
2349 pub fn page_down(&mut self) -> RowNavigationResult {
2351 let total_rows = self.dataview.row_count();
2352 let visible_rows = self.terminal_height.saturating_sub(6) as usize; debug!(target: "viewport_manager",
2356 "page_down: crosshair_row={}, total_rows={}, visible_rows={}, current_viewport_rows={:?}",
2357 self.crosshair_row, total_rows, visible_rows, self.viewport_rows);
2358
2359 if self.viewport_lock {
2361 debug!(target: "viewport_manager",
2362 "page_down: Viewport locked, moving within current viewport");
2363 let new_row = self
2365 .viewport_rows
2366 .end
2367 .saturating_sub(1)
2368 .min(total_rows.saturating_sub(1));
2369 self.crosshair_row = new_row;
2370 return RowNavigationResult {
2371 row_position: new_row,
2372 row_scroll_offset: self.viewport_rows.start,
2373 description: format!(
2374 "Page down within locked viewport: row {} → {}",
2375 self.crosshair_row + 1,
2376 new_row + 1
2377 ),
2378 viewport_changed: false,
2379 };
2380 }
2381
2382 if self.cursor_lock {
2384 if let Some(lock_position) = self.cursor_lock_position {
2385 debug!(target: "viewport_manager",
2386 "page_down: Cursor locked at position {}", lock_position);
2387
2388 let old_scroll_offset = self.viewport_rows.start;
2390 let max_scroll = total_rows.saturating_sub(visible_rows);
2391 let new_scroll_offset = (old_scroll_offset + visible_rows).min(max_scroll);
2392
2393 if new_scroll_offset == old_scroll_offset {
2394 return RowNavigationResult {
2396 row_position: self.crosshair_row,
2397 row_scroll_offset: old_scroll_offset,
2398 description: "Already at bottom".to_string(),
2399 viewport_changed: false,
2400 };
2401 }
2402
2403 self.viewport_rows =
2405 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2406
2407 self.crosshair_row =
2409 (new_scroll_offset + lock_position).min(total_rows.saturating_sub(1));
2410
2411 return RowNavigationResult {
2412 row_position: self.crosshair_row,
2413 row_scroll_offset: new_scroll_offset,
2414 description: format!(
2415 "Page down with cursor lock (viewport {} → {})",
2416 old_scroll_offset + 1,
2417 new_scroll_offset + 1
2418 ),
2419 viewport_changed: true,
2420 };
2421 }
2422 }
2423
2424 let new_row = (self.crosshair_row + visible_rows).min(total_rows.saturating_sub(1));
2427 self.crosshair_row = new_row;
2428
2429 let old_scroll_offset = self.viewport_rows.start;
2431 let new_scroll_offset = if new_row >= self.viewport_rows.start + visible_rows {
2432 (new_row + 1).saturating_sub(visible_rows)
2434 } else {
2435 old_scroll_offset
2437 };
2438
2439 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2441 let viewport_changed = new_scroll_offset != old_scroll_offset;
2442
2443 let description = format!(
2444 "Page down: row {} → {} (of {})",
2445 self.crosshair_row + 1,
2446 new_row + 1,
2447 total_rows
2448 );
2449
2450 debug!(target: "viewport_manager",
2451 "page_down result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2452 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2453
2454 RowNavigationResult {
2455 row_position: new_row,
2456 row_scroll_offset: new_scroll_offset,
2457 description,
2458 viewport_changed,
2459 }
2460 }
2461
2462 pub fn page_up(&mut self) -> RowNavigationResult {
2464 let total_rows = self.dataview.row_count();
2465 let visible_rows = self.terminal_height.saturating_sub(6) as usize; debug!(target: "viewport_manager",
2469 "page_up: crosshair_row={}, visible_rows={}, current_viewport_rows={:?}",
2470 self.crosshair_row, visible_rows, self.viewport_rows);
2471
2472 if self.viewport_lock {
2474 debug!(target: "viewport_manager",
2475 "page_up: Viewport locked, moving within current viewport");
2476 let new_row = self.viewport_rows.start;
2478 self.crosshair_row = new_row;
2479 return RowNavigationResult {
2480 row_position: new_row,
2481 row_scroll_offset: self.viewport_rows.start,
2482 description: format!(
2483 "Page up within locked viewport: row {} → {}",
2484 self.crosshair_row + 1,
2485 new_row + 1
2486 ),
2487 viewport_changed: false,
2488 };
2489 }
2490
2491 if self.cursor_lock {
2493 if let Some(lock_position) = self.cursor_lock_position {
2494 debug!(target: "viewport_manager",
2495 "page_up: Cursor locked at position {}", lock_position);
2496
2497 let old_scroll_offset = self.viewport_rows.start;
2499 let new_scroll_offset = old_scroll_offset.saturating_sub(visible_rows);
2500
2501 if new_scroll_offset == old_scroll_offset {
2502 return RowNavigationResult {
2504 row_position: self.crosshair_row,
2505 row_scroll_offset: old_scroll_offset,
2506 description: "Already at top".to_string(),
2507 viewport_changed: false,
2508 };
2509 }
2510
2511 self.viewport_rows =
2513 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2514
2515 self.crosshair_row = new_scroll_offset + lock_position;
2517
2518 return RowNavigationResult {
2519 row_position: self.crosshair_row,
2520 row_scroll_offset: new_scroll_offset,
2521 description: format!(
2522 "Page up with cursor lock (viewport {} → {})",
2523 old_scroll_offset + 1,
2524 new_scroll_offset + 1
2525 ),
2526 viewport_changed: true,
2527 };
2528 }
2529 }
2530
2531 let new_row = self.crosshair_row.saturating_sub(visible_rows);
2534 self.crosshair_row = new_row;
2535
2536 let old_scroll_offset = self.viewport_rows.start;
2538 let new_scroll_offset = if new_row < self.viewport_rows.start {
2539 new_row
2541 } else {
2542 old_scroll_offset
2544 };
2545
2546 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2548 let viewport_changed = new_scroll_offset != old_scroll_offset;
2549
2550 let description = format!("Page up: row {} → {}", self.crosshair_row + 1, new_row + 1);
2551
2552 debug!(target: "viewport_manager",
2553 "page_up result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2554 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2555
2556 RowNavigationResult {
2557 row_position: new_row,
2558 row_scroll_offset: new_scroll_offset,
2559 description,
2560 viewport_changed,
2561 }
2562 }
2563
2564 pub fn half_page_down(&mut self) -> RowNavigationResult {
2566 let total_rows = self.dataview.row_count();
2567 let visible_rows = self.terminal_height.saturating_sub(6) as usize; let half_page = visible_rows / 2;
2570
2571 debug!(target: "viewport_manager",
2572 "half_page_down: crosshair_row={}, total_rows={}, half_page={}, current_viewport_rows={:?}",
2573 self.crosshair_row, total_rows, half_page, self.viewport_rows);
2574
2575 if self.viewport_lock {
2577 debug!(target: "viewport_manager",
2578 "half_page_down: Viewport locked, moving within current viewport");
2579 let new_row = self
2581 .viewport_rows
2582 .end
2583 .saturating_sub(1)
2584 .min(total_rows.saturating_sub(1));
2585 self.crosshair_row = new_row;
2586 return RowNavigationResult {
2587 row_position: new_row,
2588 row_scroll_offset: self.viewport_rows.start,
2589 description: format!(
2590 "Half page down within locked viewport: row {} → {}",
2591 self.crosshair_row + 1,
2592 new_row + 1
2593 ),
2594 viewport_changed: false,
2595 };
2596 }
2597
2598 if self.cursor_lock {
2600 if let Some(lock_position) = self.cursor_lock_position {
2601 debug!(target: "viewport_manager",
2602 "half_page_down: Cursor locked at position {}", lock_position);
2603
2604 let old_scroll_offset = self.viewport_rows.start;
2606 let max_scroll = total_rows.saturating_sub(visible_rows);
2607 let new_scroll_offset = (old_scroll_offset + half_page).min(max_scroll);
2608
2609 if new_scroll_offset == old_scroll_offset {
2610 return RowNavigationResult {
2612 row_position: self.crosshair_row,
2613 row_scroll_offset: old_scroll_offset,
2614 description: "Already at bottom".to_string(),
2615 viewport_changed: false,
2616 };
2617 }
2618
2619 self.viewport_rows =
2621 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2622
2623 self.crosshair_row =
2625 (new_scroll_offset + lock_position).min(total_rows.saturating_sub(1));
2626
2627 return RowNavigationResult {
2628 row_position: self.crosshair_row,
2629 row_scroll_offset: new_scroll_offset,
2630 description: format!(
2631 "Half page down with cursor lock (viewport {} → {})",
2632 old_scroll_offset + 1,
2633 new_scroll_offset + 1
2634 ),
2635 viewport_changed: true,
2636 };
2637 }
2638 }
2639
2640 let new_row = (self.crosshair_row + half_page).min(total_rows.saturating_sub(1));
2643 self.crosshair_row = new_row;
2644
2645 let old_scroll_offset = self.viewport_rows.start;
2647 let new_scroll_offset = if new_row >= self.viewport_rows.start + visible_rows {
2648 (new_row + 1).saturating_sub(visible_rows)
2650 } else {
2651 old_scroll_offset
2653 };
2654
2655 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2657 let viewport_changed = new_scroll_offset != old_scroll_offset;
2658
2659 let description = format!(
2660 "Half page down: row {} → {} (of {})",
2661 self.crosshair_row + 1 - half_page.min(self.crosshair_row),
2662 new_row + 1,
2663 total_rows
2664 );
2665
2666 debug!(target: "viewport_manager",
2667 "half_page_down result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2668 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2669
2670 RowNavigationResult {
2671 row_position: new_row,
2672 row_scroll_offset: new_scroll_offset,
2673 description,
2674 viewport_changed,
2675 }
2676 }
2677
2678 pub fn half_page_up(&mut self) -> RowNavigationResult {
2680 let total_rows = self.dataview.row_count();
2681 let visible_rows = self.terminal_height.saturating_sub(6) as usize; let half_page = visible_rows / 2;
2684
2685 debug!(target: "viewport_manager",
2686 "half_page_up: crosshair_row={}, half_page={}, current_viewport_rows={:?}",
2687 self.crosshair_row, half_page, self.viewport_rows);
2688
2689 if self.viewport_lock {
2691 debug!(target: "viewport_manager",
2692 "half_page_up: Viewport locked, moving within current viewport");
2693 let new_row = self.viewport_rows.start;
2695 self.crosshair_row = new_row;
2696 return RowNavigationResult {
2697 row_position: new_row,
2698 row_scroll_offset: self.viewport_rows.start,
2699 description: format!(
2700 "Half page up within locked viewport: row {} → {}",
2701 self.crosshair_row + 1,
2702 new_row + 1
2703 ),
2704 viewport_changed: false,
2705 };
2706 }
2707
2708 if self.cursor_lock {
2710 if let Some(lock_position) = self.cursor_lock_position {
2711 debug!(target: "viewport_manager",
2712 "half_page_up: Cursor locked at position {}", lock_position);
2713
2714 let old_scroll_offset = self.viewport_rows.start;
2716 let new_scroll_offset = old_scroll_offset.saturating_sub(half_page);
2717
2718 if new_scroll_offset == old_scroll_offset {
2719 return RowNavigationResult {
2721 row_position: self.crosshair_row,
2722 row_scroll_offset: old_scroll_offset,
2723 description: "Already at top".to_string(),
2724 viewport_changed: false,
2725 };
2726 }
2727
2728 self.viewport_rows =
2730 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2731
2732 self.crosshair_row = new_scroll_offset + lock_position;
2734
2735 return RowNavigationResult {
2736 row_position: self.crosshair_row,
2737 row_scroll_offset: new_scroll_offset,
2738 description: format!(
2739 "Half page up with cursor lock (viewport {} → {})",
2740 old_scroll_offset + 1,
2741 new_scroll_offset + 1
2742 ),
2743 viewport_changed: true,
2744 };
2745 }
2746 }
2747
2748 let new_row = self.crosshair_row.saturating_sub(half_page);
2751 self.crosshair_row = new_row;
2752
2753 let old_scroll_offset = self.viewport_rows.start;
2755 let new_scroll_offset = if new_row < self.viewport_rows.start {
2756 new_row
2758 } else {
2759 old_scroll_offset
2761 };
2762
2763 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2765 let viewport_changed = new_scroll_offset != old_scroll_offset;
2766
2767 let description = format!(
2768 "Half page up: row {} → {}",
2769 self.crosshair_row + half_page + 1,
2770 new_row + 1
2771 );
2772
2773 debug!(target: "viewport_manager",
2774 "half_page_up result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2775 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2776
2777 RowNavigationResult {
2778 row_position: new_row,
2779 row_scroll_offset: new_scroll_offset,
2780 description,
2781 viewport_changed,
2782 }
2783 }
2784
2785 pub fn navigate_to_last_row(&mut self, total_rows: usize) -> RowNavigationResult {
2787 if self.viewport_lock {
2789 let last_visible = self
2791 .viewport_rows
2792 .end
2793 .saturating_sub(1)
2794 .min(total_rows.saturating_sub(1));
2795 self.crosshair_row = last_visible;
2796 return RowNavigationResult {
2797 row_position: self.crosshair_row,
2798 row_scroll_offset: self.viewport_rows.start,
2799 description: "Moved to last visible row (viewport locked)".to_string(),
2800 viewport_changed: false,
2801 };
2802 }
2803 if total_rows == 0 {
2804 return RowNavigationResult {
2805 row_position: 0,
2806 row_scroll_offset: 0,
2807 description: "No rows to navigate".to_string(),
2808 viewport_changed: false,
2809 };
2810 }
2811
2812 let visible_rows = (self.terminal_height as usize).max(10);
2815
2816 let last_row = total_rows - 1;
2818
2819 let new_scroll_offset = if total_rows > visible_rows {
2823 total_rows - visible_rows
2824 } else {
2825 0
2826 };
2827
2828 debug!(target: "viewport_manager",
2829 "navigate_to_last_row: total_rows={}, last_row={}, visible_rows={}, new_scroll_offset={}",
2830 total_rows, last_row, visible_rows, new_scroll_offset);
2831
2832 let old_scroll_offset = self.viewport_rows.start;
2834 let viewport_changed = new_scroll_offset != old_scroll_offset;
2835
2836 self.viewport_rows = new_scroll_offset..total_rows.min(new_scroll_offset + visible_rows);
2838
2839 self.crosshair_row = last_row;
2842
2843 let description = format!("Jumped to last row ({}/{})", last_row + 1, total_rows);
2844
2845 debug!(target: "viewport_manager",
2846 "navigate_to_last_row result: row={}, crosshair_row={}, scroll_offset={}→{}, viewport_changed={}",
2847 last_row, self.crosshair_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2848
2849 RowNavigationResult {
2850 row_position: last_row,
2851 row_scroll_offset: new_scroll_offset,
2852 description,
2853 viewport_changed,
2854 }
2855 }
2856
2857 pub fn navigate_to_first_row(&mut self, total_rows: usize) -> RowNavigationResult {
2859 if self.viewport_lock {
2861 self.crosshair_row = self.viewport_rows.start;
2863 return RowNavigationResult {
2864 row_position: self.crosshair_row,
2865 row_scroll_offset: self.viewport_rows.start,
2866 description: "Moved to first visible row (viewport locked)".to_string(),
2867 viewport_changed: false,
2868 };
2869 }
2870 if total_rows == 0 {
2871 return RowNavigationResult {
2872 row_position: 0,
2873 row_scroll_offset: 0,
2874 description: "No rows to navigate".to_string(),
2875 viewport_changed: false,
2876 };
2877 }
2878
2879 let visible_rows = (self.terminal_height as usize).max(10);
2882
2883 let first_row = 0;
2885
2886 let new_scroll_offset = 0;
2888
2889 debug!(target: "viewport_manager",
2890 "navigate_to_first_row: total_rows={}, visible_rows={}",
2891 total_rows, visible_rows);
2892
2893 let old_scroll_offset = self.viewport_rows.start;
2895 let viewport_changed = new_scroll_offset != old_scroll_offset;
2896
2897 self.viewport_rows = 0..visible_rows.min(total_rows);
2899
2900 self.crosshair_row = first_row;
2902
2903 let description = format!("Jumped to first row (1/{})", total_rows);
2904
2905 debug!(target: "viewport_manager",
2906 "navigate_to_first_row result: row=0, crosshair_row={}, scroll_offset={}→0, viewport_changed={}",
2907 self.crosshair_row, old_scroll_offset, viewport_changed);
2908
2909 RowNavigationResult {
2910 row_position: first_row,
2911 row_scroll_offset: new_scroll_offset,
2912 description,
2913 viewport_changed,
2914 }
2915 }
2916
2917 pub fn navigate_to_viewport_top(&mut self) -> RowNavigationResult {
2919 let top_row = self.viewport_rows.start;
2920 let old_row = self.crosshair_row;
2921
2922 self.crosshair_row = top_row;
2924
2925 let description = format!("Moved to viewport top (row {})", top_row + 1);
2926
2927 debug!(target: "viewport_manager",
2928 "navigate_to_viewport_top: crosshair {} -> {}",
2929 old_row, self.crosshair_row);
2930
2931 RowNavigationResult {
2932 row_position: self.crosshair_row,
2933 row_scroll_offset: self.viewport_rows.start,
2934 description,
2935 viewport_changed: false, }
2937 }
2938
2939 pub fn navigate_to_viewport_middle(&mut self) -> RowNavigationResult {
2941 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
2943 let middle_offset = viewport_height / 2;
2944 let middle_row = self.viewport_rows.start + middle_offset;
2945 let old_row = self.crosshair_row;
2946
2947 self.crosshair_row = middle_row;
2949
2950 let description = format!("Moved to viewport middle (row {})", middle_row + 1);
2951
2952 debug!(target: "viewport_manager",
2953 "navigate_to_viewport_middle: crosshair {} -> {}",
2954 old_row, self.crosshair_row);
2955
2956 RowNavigationResult {
2957 row_position: self.crosshair_row,
2958 row_scroll_offset: self.viewport_rows.start,
2959 description,
2960 viewport_changed: false, }
2962 }
2963
2964 pub fn navigate_to_viewport_bottom(&mut self) -> RowNavigationResult {
2966 let bottom_row = self.viewport_rows.end.saturating_sub(1);
2969 let old_row = self.crosshair_row;
2970
2971 self.crosshair_row = bottom_row;
2973
2974 let description = format!("Moved to viewport bottom (row {})", bottom_row + 1);
2975
2976 debug!(target: "viewport_manager",
2977 "navigate_to_viewport_bottom: crosshair {} -> {}",
2978 old_row, self.crosshair_row);
2979
2980 RowNavigationResult {
2981 row_position: self.crosshair_row,
2982 row_scroll_offset: self.viewport_rows.start,
2983 description,
2984 viewport_changed: false, }
2986 }
2987
2988 pub fn toggle_cursor_lock(&mut self) -> (bool, String) {
2991 self.cursor_lock = !self.cursor_lock;
2992
2993 if self.cursor_lock {
2994 let relative_position = self.crosshair_row.saturating_sub(self.viewport_rows.start);
2996 self.cursor_lock_position = Some(relative_position);
2997
2998 let description = format!(
2999 "Cursor lock: ON (locked at viewport position {})",
3000 relative_position + 1
3001 );
3002 debug!(target: "viewport_manager",
3003 "Cursor lock enabled: crosshair at viewport position {}",
3004 relative_position);
3005 (true, description)
3006 } else {
3007 self.cursor_lock_position = None;
3008 let description = "Cursor lock: OFF".to_string();
3009 debug!(target: "viewport_manager", "Cursor lock disabled");
3010 (false, description)
3011 }
3012 }
3013
3014 pub fn toggle_viewport_lock(&mut self) -> (bool, String) {
3016 self.viewport_lock = !self.viewport_lock;
3017
3018 if self.viewport_lock {
3019 self.viewport_lock_boundaries = Some(self.viewport_rows.clone());
3021
3022 let description = format!(
3023 "Viewport lock: ON (no scrolling, cursor constrained to rows {}-{})",
3024 self.viewport_rows.start + 1,
3025 self.viewport_rows.end
3026 );
3027 debug!(target: "viewport_manager",
3028 "VIEWPORT LOCK ENABLED: boundaries {:?}, crosshair={}, viewport={:?}",
3029 self.viewport_lock_boundaries, self.crosshair_row, self.viewport_rows);
3030 (true, description)
3031 } else {
3032 self.viewport_lock_boundaries = None;
3033 let description = "Viewport lock: OFF (normal scrolling)".to_string();
3034 debug!(target: "viewport_manager", "VIEWPORT LOCK DISABLED");
3035 (false, description)
3036 }
3037 }
3038
3039 pub fn is_cursor_locked(&self) -> bool {
3041 self.cursor_lock
3042 }
3043
3044 pub fn is_viewport_locked(&self) -> bool {
3046 self.viewport_lock
3047 }
3048
3049 pub fn lock_viewport(&mut self) {
3051 if !self.viewport_lock {
3052 self.viewport_lock = true;
3053 self.viewport_lock_boundaries = Some(self.viewport_rows.clone());
3054 debug!(target: "viewport_manager", "Viewport locked: rows {}-{}",
3055 self.viewport_rows.start + 1, self.viewport_rows.end);
3056 }
3057 }
3058
3059 pub fn unlock_viewport(&mut self) {
3061 if self.viewport_lock {
3062 self.viewport_lock = false;
3063 self.viewport_lock_boundaries = None;
3064 debug!(target: "viewport_manager", "Viewport unlocked");
3065 }
3066 }
3067
3068 pub fn reorder_column_left(&mut self, current_column: usize) -> ColumnReorderResult {
3070 debug!(target: "viewport_manager",
3071 "reorder_column_left: current_column={}, viewport={:?}",
3072 current_column, self.viewport_cols
3073 );
3074
3075 let column_count = self.dataview.column_count();
3077
3078 if current_column >= column_count {
3079 return ColumnReorderResult {
3080 new_column_position: current_column,
3081 description: "Invalid column position".to_string(),
3082 success: false,
3083 };
3084 }
3085
3086 let pinned_count = self.dataview.get_pinned_columns().len();
3088
3089 debug!(target: "viewport_manager",
3090 "Before move: column_count={}, pinned_count={}, current_column={}",
3091 column_count, pinned_count, current_column
3092 );
3093
3094 let mut new_dataview = (*self.dataview).clone();
3096
3097 let success = new_dataview.move_column_left(current_column);
3099
3100 if success {
3101 self.dataview = Arc::new(new_dataview);
3103 }
3104
3105 if success {
3106 self.invalidate_cache(); let wrapped_to_end =
3110 current_column == 0 || (current_column == pinned_count && pinned_count > 0);
3111 let new_position = if wrapped_to_end {
3112 column_count - 1
3114 } else {
3115 current_column - 1
3117 };
3118
3119 let column_names = self.dataview.column_names();
3120 let column_name = column_names
3121 .get(new_position)
3122 .map(|s| s.as_str())
3123 .unwrap_or("?");
3124
3125 debug!(target: "viewport_manager",
3126 "After move: new_position={}, wrapped_to_end={}, column_name={}",
3127 new_position, wrapped_to_end, column_name
3128 );
3129
3130 if wrapped_to_end {
3132 let optimal_offset = self.calculate_optimal_offset_for_last_column(
3134 self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH),
3135 );
3136 debug!(target: "viewport_manager",
3137 "Column wrapped to end! Adjusting viewport from {:?} to {}..{}",
3138 self.viewport_cols, optimal_offset, self.dataview.column_count()
3139 );
3140 self.viewport_cols = optimal_offset..self.dataview.column_count();
3141 } else {
3142 if !self.viewport_cols.contains(&new_position) {
3144 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3146
3147 let columns_that_fit =
3149 self.calculate_columns_that_fit(new_position, terminal_width);
3150
3151 let new_start = if new_position < self.viewport_cols.start {
3153 new_position
3155 } else {
3156 new_position.saturating_sub(columns_that_fit - 1)
3158 };
3159
3160 let new_end = (new_start + columns_that_fit).min(self.dataview.column_count());
3161 self.viewport_cols = new_start..new_end;
3162
3163 debug!(target: "viewport_manager",
3164 "Column moved outside viewport! Adjusting viewport to {}..{} to show column {} at position {}",
3165 new_start, new_end, column_name, new_position
3166 );
3167 }
3168 }
3169
3170 self.crosshair_col = new_position;
3172
3173 ColumnReorderResult {
3174 new_column_position: new_position,
3175 description: format!("Moved column '{}' left", column_name),
3176 success: true,
3177 }
3178 } else {
3179 ColumnReorderResult {
3180 new_column_position: current_column,
3181 description: "Cannot move column left".to_string(),
3182 success: false,
3183 }
3184 }
3185 }
3186
3187 pub fn reorder_column_right(&mut self, current_column: usize) -> ColumnReorderResult {
3189 let column_count = self.dataview.column_count();
3191
3192 if current_column >= column_count {
3193 return ColumnReorderResult {
3194 new_column_position: current_column,
3195 description: "Invalid column position".to_string(),
3196 success: false,
3197 };
3198 }
3199
3200 let pinned_count = self.dataview.get_pinned_columns().len();
3202
3203 let mut new_dataview = (*self.dataview).clone();
3205
3206 let success = new_dataview.move_column_right(current_column);
3208
3209 if success {
3210 self.dataview = Arc::new(new_dataview);
3212 }
3213
3214 if success {
3215 self.invalidate_cache(); let wrapped_to_beginning = current_column == column_count - 1
3219 || (pinned_count > 0 && current_column == pinned_count - 1);
3220
3221 let new_position = if current_column == column_count - 1 {
3222 if pinned_count > 0 {
3224 pinned_count } else {
3226 0 }
3228 } else if pinned_count > 0 && current_column == pinned_count - 1 {
3229 0
3231 } else {
3232 current_column + 1
3234 };
3235
3236 let column_names = self.dataview.column_names();
3237 let column_name = column_names
3238 .get(new_position)
3239 .map(|s| s.as_str())
3240 .unwrap_or("?");
3241
3242 if wrapped_to_beginning {
3244 self.viewport_cols = 0..self.dataview.column_count().min(20); debug!(target: "viewport_manager",
3247 "Column wrapped to beginning, resetting viewport to show column {} at position {}",
3248 column_name, new_position
3249 );
3250 } else {
3251 if !self.viewport_cols.contains(&new_position) {
3253 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3255
3256 let columns_that_fit =
3258 self.calculate_columns_that_fit(new_position, terminal_width);
3259
3260 let new_start = if new_position > self.viewport_cols.end {
3262 new_position.saturating_sub(columns_that_fit - 1)
3264 } else {
3265 new_position
3267 };
3268
3269 let new_end = (new_start + columns_that_fit).min(self.dataview.column_count());
3270 self.viewport_cols = new_start..new_end;
3271
3272 debug!(target: "viewport_manager",
3273 "Column moved outside viewport! Adjusting viewport to {}..{} to show column {} at position {}",
3274 new_start, new_end, column_name, new_position
3275 );
3276 }
3277 }
3278
3279 self.crosshair_col = new_position;
3281
3282 ColumnReorderResult {
3283 new_column_position: new_position,
3284 description: format!("Moved column '{}' right", column_name),
3285 success: true,
3286 }
3287 } else {
3288 ColumnReorderResult {
3289 new_column_position: current_column,
3290 description: "Cannot move column right".to_string(),
3291 success: false,
3292 }
3293 }
3294 }
3295
3296 pub fn hide_column(&mut self, column_index: usize) -> bool {
3299 debug!(target: "viewport_manager", "hide_column: column_index={}", column_index);
3300
3301 let mut new_dataview = (*self.dataview).clone();
3303
3304 let success = new_dataview.hide_column(column_index);
3306
3307 if success {
3308 self.dataview = Arc::new(new_dataview);
3310 self.invalidate_cache(); let column_count = self.dataview.column_count();
3314 if self.viewport_cols.end > column_count {
3315 self.viewport_cols.end = column_count;
3316 }
3317 if self.viewport_cols.start >= column_count && column_count > 0 {
3318 self.viewport_cols.start = column_count - 1;
3319 }
3320
3321 if column_index == self.crosshair_col {
3324 if column_count > 0 {
3326 if self.crosshair_col >= column_count {
3329 self.crosshair_col = column_count - 1;
3330 }
3331 } else {
3334 self.crosshair_col = 0;
3335 }
3336 debug!(target: "viewport_manager", "Crosshair was on hidden column, moved to {}", self.crosshair_col);
3337 } else if column_index < self.crosshair_col {
3338 self.crosshair_col = self.crosshair_col.saturating_sub(1);
3340 debug!(target: "viewport_manager", "Hidden column was before crosshair, adjusted crosshair to {}", self.crosshair_col);
3341 }
3342
3343 debug!(target: "viewport_manager", "Column {} hidden successfully", column_index);
3344 } else {
3345 debug!(target: "viewport_manager", "Failed to hide column {} (might be pinned)", column_index);
3346 }
3347
3348 success
3349 }
3350
3351 pub fn hide_column_by_name(&mut self, column_name: &str) -> bool {
3354 debug!(target: "viewport_manager", "hide_column_by_name: column_name={}", column_name);
3355
3356 let mut new_dataview = (*self.dataview).clone();
3358
3359 let success = new_dataview.hide_column_by_name(column_name);
3361
3362 if success {
3363 self.dataview = Arc::new(new_dataview);
3365 }
3366
3367 if success {
3368 self.invalidate_cache(); let column_count = self.dataview.column_count();
3372 if self.viewport_cols.end > column_count {
3373 self.viewport_cols.end = column_count;
3374 }
3375 if self.viewport_cols.start >= column_count && column_count > 0 {
3376 self.viewport_cols.start = column_count - 1;
3377 }
3378
3379 if self.crosshair_col >= column_count && column_count > 0 {
3381 self.crosshair_col = column_count - 1;
3382 debug!(target: "viewport_manager", "Adjusted crosshair to {} after hiding column", self.crosshair_col);
3383 }
3384
3385 debug!(target: "viewport_manager", "Column '{}' hidden successfully", column_name);
3386 } else {
3387 debug!(target: "viewport_manager", "Failed to hide column '{}' (might be pinned or not found)", column_name);
3388 }
3389
3390 success
3391 }
3392
3393 pub fn hide_empty_columns(&mut self) -> usize {
3396 debug!(target: "viewport_manager", "hide_empty_columns called");
3397
3398 let mut new_dataview = (*self.dataview).clone();
3400
3401 let count = new_dataview.hide_empty_columns();
3403
3404 if count > 0 {
3405 self.dataview = Arc::new(new_dataview);
3407 }
3408
3409 if count > 0 {
3410 self.invalidate_cache(); let column_count = self.dataview.column_count();
3414 if self.viewport_cols.end > column_count {
3415 self.viewport_cols.end = column_count;
3416 }
3417 if self.viewport_cols.start >= column_count && column_count > 0 {
3418 self.viewport_cols.start = column_count - 1;
3419 }
3420
3421 debug!(target: "viewport_manager", "Hidden {} empty columns", count);
3422 }
3423
3424 count
3425 }
3426
3427 pub fn unhide_all_columns(&mut self) {
3429 debug!(target: "viewport_manager", "unhide_all_columns called");
3430
3431 let mut new_dataview = (*self.dataview).clone();
3433
3434 new_dataview.unhide_all_columns();
3436
3437 self.dataview = Arc::new(new_dataview);
3439
3440 self.invalidate_cache(); let column_count = self.dataview.column_count();
3444 self.viewport_cols = 0..column_count.min(20); debug!(target: "viewport_manager", "All columns unhidden, viewport reset to {:?}", self.viewport_cols);
3447 }
3448
3449 pub fn pin_column(&mut self, column_index: usize) -> bool {
3452 debug!(target: "viewport_manager", "pin_column: column_index={}", column_index);
3453
3454 let mut new_dataview = (*self.dataview).clone();
3456
3457 let success = new_dataview.pin_column(column_index).is_ok();
3459
3460 if success {
3461 self.dataview = Arc::new(new_dataview);
3463 self.invalidate_cache(); debug!(target: "viewport_manager", "Column {} pinned successfully", column_index);
3466 } else {
3467 debug!(target: "viewport_manager", "Failed to pin column {}", column_index);
3468 }
3469
3470 success
3471 }
3472
3473 pub fn set_current_column(&mut self, visual_column: usize) -> bool {
3476 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH); let total_visual_columns = self.dataview.get_display_columns().len();
3478
3479 tracing::debug!("[PIN_DEBUG] === set_current_column ===");
3480 tracing::debug!(
3481 "[PIN_DEBUG] visual_column={}, viewport_cols={:?}",
3482 visual_column,
3483 self.viewport_cols
3484 );
3485 tracing::debug!(
3486 "[PIN_DEBUG] terminal_width={}, total_visual_columns={}",
3487 terminal_width,
3488 total_visual_columns
3489 );
3490
3491 debug!(target: "viewport_manager",
3492 "set_current_column ENTRY: visual_column={}, current_viewport={:?}, terminal_width={}, total_visual={}",
3493 visual_column, self.viewport_cols, terminal_width, total_visual_columns);
3494
3495 if visual_column >= total_visual_columns {
3497 debug!(target: "viewport_manager", "Visual column {} out of bounds (max {})", visual_column, total_visual_columns);
3498 tracing::debug!(
3499 "[PIN_DEBUG] Column {} out of bounds (max {})",
3500 visual_column,
3501 total_visual_columns
3502 );
3503 return false;
3504 }
3505
3506 self.crosshair_col = visual_column;
3508 debug!(target: "viewport_manager", "Updated crosshair_col to {}", visual_column);
3509 tracing::debug!("[PIN_DEBUG] Updated crosshair_col to {}", visual_column);
3510
3511 let display_columns = self.dataview.get_display_columns();
3514 let mut total_width_needed = 0u16;
3515 for &dt_idx in &display_columns {
3516 let width =
3517 self.width_calculator
3518 .get_column_width(&self.dataview, &self.viewport_rows, dt_idx);
3519 total_width_needed += width + 1; }
3521
3522 if total_width_needed <= terminal_width {
3523 debug!(target: "viewport_manager",
3525 "Visual column {} in optimal layout mode (all columns fit), no adjustment needed", visual_column);
3526 tracing::debug!("[PIN_DEBUG] All columns fit, no adjustment needed");
3527 tracing::debug!("[PIN_DEBUG] === End set_current_column (all fit) ===");
3528 return false;
3529 }
3530
3531 let pinned_count = self.dataview.get_pinned_columns().len();
3534 tracing::debug!("[PIN_DEBUG] pinned_count={}", pinned_count);
3535
3536 let visible_columns = self.calculate_visible_column_indices(terminal_width);
3538 let display_columns = self.dataview.get_display_columns();
3539
3540 let target_dt_idx = if visual_column < display_columns.len() {
3542 display_columns[visual_column]
3543 } else {
3544 tracing::debug!("[PIN_DEBUG] Column {} out of bounds", visual_column);
3545 return false;
3546 };
3547
3548 let is_visible = visible_columns.contains(&target_dt_idx);
3549 tracing::debug!(
3550 "[PIN_DEBUG] Column {} (dt_idx={}) visible check: visible_columns={:?}, is_visible={}",
3551 visual_column,
3552 target_dt_idx,
3553 visible_columns,
3554 is_visible
3555 );
3556
3557 debug!(target: "viewport_manager",
3558 "set_current_column CHECK: visual_column={}, viewport={:?}, is_visible={}",
3559 visual_column, self.viewport_cols, is_visible);
3560
3561 if is_visible {
3562 debug!(target: "viewport_manager", "Visual column {} already visible in viewport {:?}, no adjustment needed",
3563 visual_column, self.viewport_cols);
3564 tracing::debug!("[PIN_DEBUG] Column already visible, no adjustment");
3565 tracing::debug!("[PIN_DEBUG] === End set_current_column (no change) ===");
3566 return false;
3567 }
3568
3569 debug!(target: "viewport_manager", "Visual column {} NOT visible, calculating new offset", visual_column);
3571 let new_scroll_offset = self.calculate_scroll_offset_for_visual_column(visual_column);
3572 let old_scroll_offset = self.viewport_cols.start;
3573
3574 debug!(target: "viewport_manager", "Calculated new_scroll_offset={}, old_scroll_offset={}",
3575 new_scroll_offset, old_scroll_offset);
3576
3577 if new_scroll_offset != old_scroll_offset {
3578 let display_columns = self.dataview.get_display_columns();
3581 let pinned_count = self.dataview.get_pinned_columns().len();
3582 let mut used_width = 0u16;
3583 let separator_width = 1u16;
3584
3585 for visual_idx in 0..pinned_count {
3587 if visual_idx < display_columns.len() {
3588 let dt_idx = display_columns[visual_idx];
3589 let width = self.width_calculator.get_column_width(
3590 &self.dataview,
3591 &self.viewport_rows,
3592 dt_idx,
3593 );
3594 used_width += width + separator_width;
3595 }
3596 }
3597
3598 let mut scrollable_columns_that_fit = 0;
3600 let visual_start = pinned_count + new_scroll_offset;
3601
3602 for visual_idx in visual_start..display_columns.len() {
3603 let dt_idx = display_columns[visual_idx];
3604 let width = self.width_calculator.get_column_width(
3605 &self.dataview,
3606 &self.viewport_rows,
3607 dt_idx,
3608 );
3609 if used_width + width + separator_width <= terminal_width {
3610 used_width += width + separator_width;
3611 scrollable_columns_that_fit += 1;
3612 } else {
3613 break;
3614 }
3615 }
3616
3617 let new_end = new_scroll_offset + scrollable_columns_that_fit;
3619 self.viewport_cols = new_scroll_offset..new_end;
3620 self.cache_dirty = true; debug!(target: "viewport_manager",
3623 "Adjusted viewport for visual column {}: offset {}→{} (viewport: {:?})",
3624 visual_column, old_scroll_offset, new_scroll_offset, self.viewport_cols);
3625
3626 return true;
3627 }
3628
3629 false
3630 }
3631
3632 fn calculate_visible_column_indices_with_offset(
3635 &mut self,
3636 available_width: u16,
3637 scroll_offset: usize,
3638 ) -> Vec<usize> {
3639 let original_viewport = self.viewport_cols.clone();
3641 let total_visual_columns = self.dataview.get_display_columns().len();
3642 self.viewport_cols = scroll_offset..(scroll_offset + 50).min(total_visual_columns);
3643
3644 let visible_columns = self.calculate_visible_column_indices(available_width);
3645
3646 self.viewport_cols = original_viewport;
3648
3649 visible_columns
3650 }
3651
3652 fn calculate_scroll_offset_for_visual_column(&mut self, visual_column: usize) -> usize {
3655 debug!(target: "viewport_manager",
3656 "=== calculate_scroll_offset_for_visual_column ENTRY ===");
3657 debug!(target: "viewport_manager",
3658 "visual_column={}, current_viewport={:?}", visual_column, self.viewport_cols);
3659
3660 let pinned_count = self.dataview.get_pinned_columns().len();
3661 debug!(target: "viewport_manager",
3662 "pinned_count={}", pinned_count);
3663
3664 if visual_column < pinned_count {
3666 debug!(target: "viewport_manager",
3667 "Visual column {} is pinned, returning current offset {}",
3668 visual_column, self.viewport_cols.start);
3669 return self.viewport_cols.start; }
3671
3672 let scrollable_column = visual_column - pinned_count;
3674 debug!(target: "viewport_manager",
3675 "Converted to scrollable_column={}", scrollable_column);
3676
3677 let current_scroll_offset = self.viewport_cols.start;
3678 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3679
3680 let display_columns = self.dataview.get_display_columns();
3682 let mut pinned_width = 0u16;
3683 let separator_width = 1u16;
3684
3685 for visual_idx in 0..pinned_count {
3686 if visual_idx < display_columns.len() {
3687 let dt_idx = display_columns[visual_idx];
3688 let width = self.width_calculator.get_column_width(
3689 &self.dataview,
3690 &self.viewport_rows,
3691 dt_idx,
3692 );
3693 pinned_width += width + separator_width;
3694 }
3695 }
3696
3697 let available_for_scrollable = terminal_width.saturating_sub(pinned_width);
3699
3700 debug!(target: "viewport_manager",
3701 "Scroll offset calculation: target_scrollable_col={}, current_offset={}, available_width={}",
3702 scrollable_column, current_scroll_offset, available_for_scrollable);
3703
3704 if scrollable_column < current_scroll_offset {
3706 debug!(target: "viewport_manager", "Column {} is left of viewport, scrolling left to offset {}",
3708 scrollable_column, scrollable_column);
3709 scrollable_column
3710 } else {
3711 debug!(target: "viewport_manager",
3715 "Checking if column {} can be made visible with minimal scrolling from offset {}",
3716 scrollable_column, current_scroll_offset);
3717
3718 let mut test_scroll_offset = current_scroll_offset;
3720 let max_scrollable_columns = display_columns.len().saturating_sub(pinned_count);
3721
3722 while test_scroll_offset <= scrollable_column
3723 && test_scroll_offset < max_scrollable_columns
3724 {
3725 let mut used_width = 0u16;
3726 let mut target_column_fits = false;
3727
3728 for test_scrollable_idx in test_scroll_offset..max_scrollable_columns {
3730 let visual_idx = pinned_count + test_scrollable_idx;
3731 if visual_idx < display_columns.len() {
3732 let dt_idx = display_columns[visual_idx];
3733 let width = self.width_calculator.get_column_width(
3734 &self.dataview,
3735 &self.viewport_rows,
3736 dt_idx,
3737 );
3738
3739 if used_width + width + separator_width <= available_for_scrollable {
3740 used_width += width + separator_width;
3741 if test_scrollable_idx == scrollable_column {
3742 target_column_fits = true;
3743 break; }
3745 } else {
3746 break; }
3748 }
3749 }
3750
3751 debug!(target: "viewport_manager",
3752 "Testing scroll_offset={}: target_fits={}, used_width={}",
3753 test_scroll_offset, target_column_fits, used_width);
3754
3755 if target_column_fits {
3756 debug!(target: "viewport_manager",
3757 "Found minimal scroll offset {} for column {} (current was {})",
3758 test_scroll_offset, scrollable_column, current_scroll_offset);
3759 return test_scroll_offset;
3760 }
3761
3762 test_scroll_offset += 1;
3764 }
3765
3766 debug!(target: "viewport_manager",
3768 "Could not find minimal scroll, placing column {} at scroll offset {}",
3769 scrollable_column, scrollable_column);
3770 scrollable_column
3771 }
3772 }
3773
3774 pub fn goto_line(&mut self, target_row: usize) -> RowNavigationResult {
3776 let total_rows = self.dataview.row_count();
3777
3778 let target_row = target_row.min(total_rows.saturating_sub(1));
3780
3781 let visible_rows = (self.terminal_height as usize).saturating_sub(6);
3783
3784 let centered_scroll_offset = if visible_rows > 0 {
3786 let half_viewport = visible_rows / 2;
3788 if target_row > half_viewport {
3789 (target_row - half_viewport).min(total_rows.saturating_sub(visible_rows))
3791 } else {
3792 0
3794 }
3795 } else {
3796 target_row
3797 };
3798
3799 let old_scroll_offset = self.viewport_rows.start;
3801 self.viewport_rows =
3802 centered_scroll_offset..(centered_scroll_offset + visible_rows).min(total_rows);
3803 let viewport_changed = centered_scroll_offset != old_scroll_offset;
3804
3805 self.crosshair_row = target_row;
3807
3808 let description = format!(
3809 "Jumped to row {} (centered at viewport {})",
3810 target_row + 1,
3811 centered_scroll_offset + 1
3812 );
3813
3814 debug!(target: "viewport_manager",
3815 "goto_line: target_row={}, crosshair_row={}, scroll_offset={}→{}, viewport={:?}",
3816 target_row, self.crosshair_row, old_scroll_offset, centered_scroll_offset, self.viewport_rows);
3817
3818 RowNavigationResult {
3819 row_position: target_row,
3820 row_scroll_offset: centered_scroll_offset,
3821 description,
3822 viewport_changed,
3823 }
3824 }
3825
3826 pub fn hide_current_column_with_result(&mut self) -> ColumnOperationResult {
3830 let visual_col_idx = self.get_crosshair_col();
3831 let columns = self.dataview.column_names();
3832
3833 if visual_col_idx >= columns.len() {
3834 return ColumnOperationResult::failure("Invalid column position");
3835 }
3836
3837 let col_name = columns[visual_col_idx].clone();
3838 let visible_count = columns.len();
3839
3840 if visible_count <= 1 {
3842 return ColumnOperationResult::failure("Cannot hide the last visible column");
3843 }
3844
3845 let success = self.hide_column(visual_col_idx);
3847
3848 if success {
3849 let mut result =
3850 ColumnOperationResult::success(format!("Column '{}' hidden", col_name));
3851 result.updated_dataview = Some(self.clone_dataview());
3852 result.new_column_position = Some(self.get_crosshair_col());
3853 result.new_viewport = Some(self.viewport_cols.clone());
3854 result
3855 } else {
3856 ColumnOperationResult::failure(format!(
3857 "Cannot hide column '{}' (may be pinned)",
3858 col_name
3859 ))
3860 }
3861 }
3862
3863 pub fn unhide_all_columns_with_result(&mut self) -> ColumnOperationResult {
3865 let hidden_columns = self.dataview.get_hidden_column_names();
3866 let count = hidden_columns.len();
3867
3868 if count == 0 {
3869 return ColumnOperationResult::success("No hidden columns");
3870 }
3871
3872 self.unhide_all_columns();
3873
3874 let mut result = ColumnOperationResult::success(format!("Unhidden {} column(s)", count));
3875 result.updated_dataview = Some(self.clone_dataview());
3876 result.affected_count = Some(count);
3877 result.new_viewport = Some(self.viewport_cols.clone());
3878 result
3879 }
3880
3881 pub fn reorder_column_left_with_result(&mut self) -> ColumnOperationResult {
3883 let current_col = self.get_crosshair_col();
3884 let reorder_result = self.reorder_column_left(current_col);
3885
3886 if reorder_result.success {
3887 let mut result = ColumnOperationResult::success(reorder_result.description);
3888 result.updated_dataview = Some(self.clone_dataview());
3889 result.new_column_position = Some(reorder_result.new_column_position);
3890 result.new_viewport = Some(self.viewport_cols.clone());
3891 result
3892 } else {
3893 ColumnOperationResult::failure(reorder_result.description)
3894 }
3895 }
3896
3897 pub fn reorder_column_right_with_result(&mut self) -> ColumnOperationResult {
3899 let current_col = self.get_crosshair_col();
3900 let reorder_result = self.reorder_column_right(current_col);
3901
3902 if reorder_result.success {
3903 let mut result = ColumnOperationResult::success(reorder_result.description);
3904 result.updated_dataview = Some(self.clone_dataview());
3905 result.new_column_position = Some(reorder_result.new_column_position);
3906 result.new_viewport = Some(self.viewport_cols.clone());
3907 result
3908 } else {
3909 ColumnOperationResult::failure(reorder_result.description)
3910 }
3911 }
3912
3913 pub fn calculate_viewport_column_widths(
3918 &mut self,
3919 viewport_start: usize,
3920 viewport_end: usize,
3921 compact_mode: bool,
3922 ) -> Vec<u16> {
3923 let headers = self.dataview.column_names();
3924 let mut widths = Vec::with_capacity(headers.len());
3925
3926 let min_width = if compact_mode { 4 } else { 6 };
3928 let padding = if compact_mode { 1 } else { 2 };
3929
3930 let available_width = self.terminal_width.saturating_sub(10) as usize;
3932 let visible_cols = headers.len().min(12); let dynamic_max = if visible_cols > 0 {
3936 (available_width / visible_cols).max(30).min(80)
3937 } else {
3938 30
3939 };
3940
3941 let max_width = if compact_mode {
3942 dynamic_max.min(40)
3943 } else {
3944 dynamic_max
3945 };
3946
3947 let mut rows_to_check = Vec::new();
3949 let source_table = self.dataview.source();
3950 for i in viewport_start..viewport_end.min(source_table.row_count()) {
3951 if let Some(row_strings) = source_table.get_row_as_strings(i) {
3952 rows_to_check.push(row_strings);
3953 }
3954 }
3955
3956 for (col_idx, header) in headers.iter().enumerate() {
3958 let mut max_col_width = header.len();
3960
3961 for row in &rows_to_check {
3963 if let Some(value) = row.get(col_idx) {
3964 let display_value = if value.is_empty() {
3965 "NULL"
3966 } else {
3967 value.as_str()
3968 };
3969 max_col_width = max_col_width.max(display_value.len());
3970 }
3971 }
3972
3973 let width = (max_col_width + padding).clamp(min_width, max_width) as u16;
3975 widths.push(width);
3976 }
3977
3978 widths
3979 }
3980
3981 pub fn calculate_optimal_column_widths(&mut self) -> Vec<u16> {
3984 self.width_calculator.calculate_with_terminal_width(
3986 &self.dataview,
3987 &self.viewport_rows,
3988 self.terminal_width,
3989 );
3990
3991 let col_count = self.dataview.column_count();
3993 let mut widths = Vec::with_capacity(col_count);
3994 for idx in 0..col_count {
3995 widths.push(self.width_calculator.get_column_width(
3996 &self.dataview,
3997 &self.viewport_rows,
3998 idx,
3999 ));
4000 }
4001 widths
4002 }
4003
4004 pub fn ensure_column_visible(&mut self, column_index: usize, available_width: u16) {
4006 debug!(target: "viewport_manager", "ensure_column_visible: column_index={}, available_width={}", column_index, available_width);
4007
4008 let total_columns = self.dataview.get_display_columns().len();
4009
4010 if column_index >= total_columns {
4012 debug!(target: "viewport_manager", "Column index {} out of range (max {})", column_index, total_columns.saturating_sub(1));
4013 return;
4014 }
4015
4016 let visible_columns = self.calculate_visible_column_indices(available_width);
4018 let dt_columns = self.dataview.get_display_columns();
4019
4020 if let Some(&dt_index) = dt_columns.get(column_index) {
4022 if visible_columns.contains(&dt_index) {
4023 debug!(target: "viewport_manager", "Column {} already visible", column_index);
4024 return;
4025 }
4026 }
4027
4028 if self.set_current_column(column_index) {
4031 self.crosshair_col = column_index;
4032 debug!(target: "viewport_manager", "Ensured column {} is visible and set crosshair", column_index);
4033 } else {
4034 debug!(target: "viewport_manager", "Failed to make column {} visible", column_index);
4035 }
4036 }
4037
4038 pub fn reorder_column(&mut self, from_index: usize, to_index: usize) -> bool {
4040 debug!(target: "viewport_manager", "reorder_column: from_index={}, to_index={}", from_index, to_index);
4041
4042 if from_index == to_index {
4043 return true; }
4045
4046 let mut new_dataview = (*self.dataview).clone();
4048
4049 let mut current_pos = from_index;
4050 let mut success = true;
4051
4052 if from_index < to_index {
4054 while current_pos < to_index && success {
4056 success = new_dataview.move_column_right(current_pos);
4057 if success {
4058 current_pos += 1;
4059 }
4060 }
4061 } else {
4062 while current_pos > to_index && success {
4064 success = new_dataview.move_column_left(current_pos);
4065 if success {
4066 current_pos -= 1;
4067 }
4068 }
4069 }
4070
4071 if success {
4072 self.dataview = Arc::new(new_dataview);
4074 self.invalidate_cache(); debug!(target: "viewport_manager", "Column moved from {} to {} successfully", from_index, to_index);
4077 } else {
4078 debug!(target: "viewport_manager", "Failed to move column from {} to {}", from_index, to_index);
4079 }
4080
4081 success
4082 }
4083
4084 pub fn calculate_column_widths(&mut self, available_width: u16) -> Vec<u16> {
4087 let _visible_indices = self.calculate_visible_column_indices(available_width);
4089
4090 self.get_column_widths().to_vec()
4092 }
4093}
4094
4095#[derive(Debug, Clone)]
4097pub struct ViewportEfficiency {
4098 pub available_width: u16,
4099 pub used_width: u16,
4100 pub wasted_space: u16,
4101 pub efficiency_percent: u8,
4102 pub visible_columns: usize,
4103 pub column_widths: Vec<u16>,
4104 pub next_column_width: Option<u16>, pub columns_that_could_fit: Vec<(usize, u16)>, }
4107
4108impl ViewportEfficiency {
4109 pub fn to_status_string(&self) -> String {
4111 format!(
4112 "Viewport: {}w used of {}w ({}% efficient, {} cols, {}w wasted)",
4113 self.used_width,
4114 self.available_width,
4115 self.efficiency_percent,
4116 self.visible_columns,
4117 self.wasted_space
4118 )
4119 }
4120
4121 pub fn to_debug_string(&self) -> String {
4123 let avg_width = if !self.column_widths.is_empty() {
4124 self.column_widths.iter().sum::<u16>() / self.column_widths.len() as u16
4125 } else {
4126 0
4127 };
4128
4129 let mut efficiency_analysis = String::new();
4131 if let Some(next_width) = self.next_column_width {
4132 efficiency_analysis.push_str(&format!(
4133 "\n\n Next column needs: {}w (+1 separator)",
4134 next_width
4135 ));
4136 if next_width < self.wasted_space {
4137 efficiency_analysis.push_str(" ✓ FITS!");
4138 } else {
4139 efficiency_analysis.push_str(&format!(" ✗ Too wide (have {}w)", self.wasted_space));
4140 }
4141 }
4142
4143 if !self.columns_that_could_fit.is_empty() {
4144 efficiency_analysis.push_str(&format!(
4145 "\n Columns that COULD fit in {}w:",
4146 self.wasted_space
4147 ));
4148 for (idx, width) in
4149 &self.columns_that_could_fit[..self.columns_that_could_fit.len().min(5)]
4150 {
4151 efficiency_analysis.push_str(&format!("\n - Column {}: {}w", idx, width));
4152 }
4153 if self.columns_that_could_fit.len() > 5 {
4154 efficiency_analysis.push_str(&format!(
4155 "\n ... and {} more",
4156 self.columns_that_could_fit.len() - 5
4157 ));
4158 }
4159 }
4160
4161 efficiency_analysis.push_str("\n\n Hypothetical efficiencies:");
4163 for extra in 1..=3 {
4164 let hypothetical_used =
4165 self.used_width + (extra * (avg_width + 1)).min(self.wasted_space);
4166 let hypothetical_eff =
4167 ((hypothetical_used as f32 / self.available_width as f32) * 100.0) as u8;
4168 let hypothetical_wasted = self.available_width.saturating_sub(hypothetical_used);
4169 efficiency_analysis.push_str(&format!(
4170 "\n +{} cols ({}w each): {}% efficiency, {}w wasted",
4171 extra, avg_width, hypothetical_eff, hypothetical_wasted
4172 ));
4173 }
4174
4175 format!(
4176 "Viewport Efficiency:\n Available: {}w\n Used: {}w\n Wasted: {}w\n Efficiency: {}%\n Columns: {} visible\n Widths: {:?}\n Avg Width: {}w{}",
4177 self.available_width,
4178 self.used_width,
4179 self.wasted_space,
4180 self.efficiency_percent,
4181 self.visible_columns,
4182 self.column_widths.clone(),
4183 avg_width,
4184 efficiency_analysis
4185 )
4186 }
4187}
4188
4189#[cfg(test)]
4190mod tests {
4191 use super::*;
4192 use crate::data::datatable::{DataColumn, DataRow, DataTable, DataValue};
4193
4194 fn create_test_dataview() -> Arc<DataView> {
4195 let mut table = DataTable::new("test");
4196 table.add_column(DataColumn::new("id"));
4197 table.add_column(DataColumn::new("name"));
4198 table.add_column(DataColumn::new("amount"));
4199
4200 for i in 0..100 {
4201 table
4202 .add_row(DataRow::new(vec![
4203 DataValue::Integer(i),
4204 DataValue::String(format!("Item {}", i)),
4205 DataValue::Float(i as f64 * 10.5),
4206 ]))
4207 .unwrap();
4208 }
4209
4210 Arc::new(DataView::new(Arc::new(table)))
4211 }
4212
4213 #[test]
4214 fn test_viewport_basic() {
4215 let dataview = create_test_dataview();
4216 let mut viewport = ViewportManager::new(dataview);
4217
4218 viewport.set_viewport(0, 0, 80, 24);
4219
4220 assert_eq!(viewport.viewport_rows(), 0..24);
4221 assert_eq!(viewport.viewport_cols(), 0..3);
4222
4223 let visible_rows = viewport.get_visible_rows();
4224 assert_eq!(visible_rows.len(), 24);
4225 }
4226
4227 #[test]
4228 fn test_column_width_calculation() {
4229 let dataview = create_test_dataview();
4230 let mut viewport = ViewportManager::new(dataview);
4231
4232 viewport.set_viewport(0, 0, 80, 10);
4233
4234 let widths = viewport.get_column_widths();
4235 assert_eq!(widths.len(), 3);
4236
4237 assert!(widths[0] < 10);
4239 assert!(widths[1] > widths[0]);
4241 }
4242
4243 #[test]
4244 fn test_viewport_scrolling() {
4245 let dataview = create_test_dataview();
4246 let mut viewport = ViewportManager::new(dataview);
4247
4248 viewport.set_viewport(0, 0, 80, 24);
4249 viewport.scroll_by(10, 0);
4250
4251 assert_eq!(viewport.viewport_rows(), 10..34);
4252
4253 viewport.scroll_by(-5, 1);
4254 assert_eq!(viewport.viewport_rows(), 5..29);
4255 assert_eq!(viewport.viewport_cols(), 1..3);
4256 }
4257
4258 #[test]
4261 fn test_navigate_to_last_and_first_column() {
4262 let dataview = create_test_dataview();
4263 let mut vm = ViewportManager::new(dataview);
4264 vm.update_terminal_size(120, 40);
4265
4266 let result = vm.navigate_to_last_column();
4268 assert_eq!(vm.get_crosshair_col(), 2); assert_eq!(result.column_position, 2);
4270
4271 let result = vm.navigate_to_first_column();
4273 assert_eq!(vm.get_crosshair_col(), 0);
4274 assert_eq!(result.column_position, 0);
4275 }
4276
4277 #[test]
4278 fn test_column_reorder_right_with_crosshair() {
4279 let dataview = create_test_dataview();
4280 let mut vm = ViewportManager::new(dataview);
4281 vm.update_terminal_size(120, 40);
4282
4283 vm.crosshair_col = 0;
4285
4286 let result = vm.reorder_column_right(0);
4288 assert!(result.success);
4289 assert_eq!(result.new_column_position, 1);
4290 assert_eq!(vm.get_crosshair_col(), 1); let headers = vm.dataview.column_names();
4294 assert_eq!(headers[0], "name"); assert_eq!(headers[1], "id"); }
4297
4298 #[test]
4299 fn test_column_reorder_left_with_crosshair() {
4300 let dataview = create_test_dataview();
4301 let mut vm = ViewportManager::new(dataview);
4302 vm.update_terminal_size(120, 40);
4303
4304 vm.crosshair_col = 1;
4306
4307 let result = vm.reorder_column_left(1);
4309 assert!(result.success);
4310 assert_eq!(result.new_column_position, 0);
4311 assert_eq!(vm.get_crosshair_col(), 0); }
4313
4314 #[test]
4315 fn test_hide_column_adjusts_crosshair() {
4316 let dataview = create_test_dataview();
4317 let mut vm = ViewportManager::new(dataview);
4318 vm.update_terminal_size(120, 40);
4319
4320 vm.crosshair_col = 1; let success = vm.hide_column(1);
4323 assert!(success);
4324 assert_eq!(vm.get_crosshair_col(), 1);
4326 assert_eq!(vm.dataview.column_count(), 2); vm.crosshair_col = 1; let success = vm.hide_column(1);
4331 assert!(success);
4332 assert_eq!(vm.get_crosshair_col(), 0); assert_eq!(vm.dataview.column_count(), 1); }
4335
4336 #[test]
4337 fn test_goto_line_centers_viewport() {
4338 let dataview = create_test_dataview();
4339 let mut vm = ViewportManager::new(dataview);
4340 vm.update_terminal_size(120, 40);
4341
4342 let result = vm.goto_line(50);
4344 assert_eq!(result.row_position, 50);
4345 assert_eq!(vm.get_crosshair_row(), 50);
4346
4347 let visible_rows = 34; let expected_offset = 50 - (visible_rows / 2);
4350 assert_eq!(result.row_scroll_offset, expected_offset);
4351 }
4352
4353 #[test]
4354 fn test_page_navigation() {
4355 let dataview = create_test_dataview();
4356 let mut vm = ViewportManager::new(dataview);
4357 vm.update_terminal_size(120, 40);
4358
4359 let initial_row = vm.get_crosshair_row();
4361 let result = vm.page_down();
4362 assert!(result.row_position > initial_row);
4363 assert_eq!(vm.get_crosshair_row(), result.row_position);
4364
4365 vm.page_down(); vm.page_down();
4368 let prev_position = vm.get_crosshair_row();
4369 let result = vm.page_up();
4370 assert!(result.row_position < prev_position); }
4372
4373 #[test]
4374 fn test_cursor_lock_mode() {
4375 let dataview = create_test_dataview();
4376 let mut vm = ViewportManager::new(dataview);
4377 vm.update_terminal_size(120, 40);
4378
4379 vm.toggle_cursor_lock();
4381 assert!(vm.is_cursor_locked());
4382
4383 let initial_viewport_position = vm.get_crosshair_row() - vm.viewport_rows.start;
4385 let result = vm.navigate_row_down();
4386
4387 if result.viewport_changed {
4389 let new_viewport_position = vm.get_crosshair_row() - vm.viewport_rows.start;
4390 assert_eq!(initial_viewport_position, new_viewport_position);
4391 }
4392 }
4393
4394 #[test]
4395 fn test_viewport_lock_prevents_scrolling() {
4396 let dataview = create_test_dataview();
4397 let mut vm = ViewportManager::new(dataview);
4398 vm.update_terminal_size(120, 40);
4399
4400 vm.toggle_viewport_lock();
4402 assert!(vm.is_viewport_locked());
4403
4404 let initial_viewport = vm.viewport_rows.clone();
4406 let result = vm.navigate_row_down();
4407
4408 assert_eq!(vm.viewport_rows, initial_viewport);
4410 assert!(!result.viewport_changed);
4412 }
4413
4414 #[test]
4415 fn test_h_m_l_viewport_navigation() {
4416 let dataview = create_test_dataview();
4417 let mut vm = ViewportManager::new(dataview);
4418 vm.update_terminal_size(120, 40);
4419
4420 for _ in 0..20 {
4422 vm.navigate_row_down();
4423 }
4424
4425 let result = vm.navigate_to_viewport_top();
4427 assert_eq!(vm.get_crosshair_row(), vm.viewport_rows.start);
4428
4429 let result = vm.navigate_to_viewport_bottom();
4431 assert_eq!(vm.get_crosshair_row(), vm.viewport_rows.end - 1);
4432
4433 let result = vm.navigate_to_viewport_middle();
4435 let expected_middle =
4436 vm.viewport_rows.start + (vm.viewport_rows.end - vm.viewport_rows.start) / 2;
4437 assert_eq!(vm.get_crosshair_row(), expected_middle);
4438 }
4439
4440 #[test]
4441 fn test_out_of_order_column_navigation() {
4442 let mut table = DataTable::new("test");
4444 for i in 0..12 {
4445 table.add_column(DataColumn::new(&format!("col{}", i)));
4446 }
4447
4448 for row in 0..10 {
4450 let mut values = Vec::new();
4451 for col in 0..12 {
4452 values.push(DataValue::String(format!("r{}c{}", row, col)));
4453 }
4454 table.add_row(DataRow::new(values)).unwrap();
4455 }
4456
4457 let dataview =
4461 DataView::new(Arc::new(table)).with_columns(vec![11, 0, 5, 3, 8, 1, 10, 2, 7, 4, 9, 6]);
4462
4463 let mut vm = ViewportManager::new(Arc::new(dataview));
4464 vm.update_terminal_size(200, 40); let column_names = vm.dataview.column_names();
4468 assert_eq!(
4469 column_names[0], "col11",
4470 "First visual column should be col11"
4471 );
4472 assert_eq!(
4473 column_names[1], "col0",
4474 "Second visual column should be col0"
4475 );
4476 assert_eq!(
4477 column_names[2], "col5",
4478 "Third visual column should be col5"
4479 );
4480
4481 vm.crosshair_col = 0;
4483
4484 let mut visual_positions = vec![0];
4486 let mut datatable_positions = vec![];
4487
4488 let display_cols = vm.dataview.get_display_columns();
4490 datatable_positions.push(display_cols[0]);
4491
4492 for i in 0..11 {
4494 let current_visual = vm.get_crosshair_col();
4495 let result = vm.navigate_column_right(current_visual);
4496
4497 let new_visual = vm.get_crosshair_col();
4499 assert_eq!(
4500 new_visual,
4501 current_visual + 1,
4502 "Crosshair should move from visual position {} to {}, but got {}",
4503 current_visual,
4504 current_visual + 1,
4505 new_visual
4506 );
4507
4508 visual_positions.push(new_visual);
4509 let display_cols = vm.dataview.get_display_columns();
4511 datatable_positions.push(display_cols[new_visual]);
4512 }
4513
4514 assert_eq!(
4516 visual_positions,
4517 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
4518 "Crosshair should move through visual positions sequentially"
4519 );
4520
4521 assert_eq!(
4523 datatable_positions,
4524 vec![11, 0, 5, 3, 8, 1, 10, 2, 7, 4, 9, 6],
4525 "DataTable indices should match our column selection order"
4526 );
4527
4528 for _i in (0..11).rev() {
4530 let current_visual = vm.get_crosshair_col();
4531 let _result = vm.navigate_column_left(current_visual);
4532
4533 let new_visual = vm.get_crosshair_col();
4535 assert_eq!(
4536 new_visual,
4537 current_visual - 1,
4538 "Crosshair should move from visual position {} to {}, but got {}",
4539 current_visual,
4540 current_visual - 1,
4541 new_visual
4542 );
4543 }
4544
4545 assert_eq!(
4547 vm.get_crosshair_col(),
4548 0,
4549 "Should be back at first visual column"
4550 );
4551
4552 vm.hide_column(2); vm.crosshair_col = 0;
4557 let _result1 = vm.navigate_column_right(0);
4558 assert_eq!(vm.get_crosshair_col(), 1, "Should be at visual position 1");
4559
4560 let _result2 = vm.navigate_column_right(1);
4561 assert_eq!(
4562 vm.get_crosshair_col(),
4563 2,
4564 "Should be at visual position 2 after hiding"
4565 );
4566
4567 let visible_cols = vm.dataview.column_names();
4569 assert_eq!(
4570 visible_cols[2], "col3",
4571 "Column at position 2 should be col3 after hiding col5"
4572 );
4573 }
4574}