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, 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 #[must_use]
162 pub fn get_viewport_range(&self) -> std::ops::Range<usize> {
163 self.viewport_cols.clone()
164 }
165
166 #[must_use]
168 pub fn get_viewport_rows(&self) -> std::ops::Range<usize> {
169 self.viewport_rows.clone()
170 }
171
172 pub fn set_crosshair(&mut self, row: usize, col: usize) {
174 self.crosshair_row = row;
175 self.crosshair_col = col;
176 debug!(target: "viewport_manager",
177 "Crosshair set to visual position: row={}, col={}", row, col);
178 }
179
180 pub fn set_crosshair_row(&mut self, row: usize) {
182 let total_rows = self.dataview.row_count();
183
184 let clamped_row = row.min(total_rows.saturating_sub(1));
186 self.crosshair_row = clamped_row;
187
188 if self.viewport_lock {
190 debug!(target: "viewport_manager",
191 "Crosshair row set to: {} (viewport locked, no scroll adjustment)",
192 clamped_row);
193 return;
194 }
195
196 let viewport_height = self.viewport_rows.len();
198 let mut viewport_changed = false;
199
200 if clamped_row < self.viewport_rows.start {
201 self.viewport_rows = clamped_row..(clamped_row + viewport_height).min(total_rows);
203 viewport_changed = true;
204 } else if clamped_row >= self.viewport_rows.end {
205 let new_start = clamped_row.saturating_sub(viewport_height.saturating_sub(1));
207 self.viewport_rows = new_start..(new_start + viewport_height).min(total_rows);
208 viewport_changed = true;
209 }
210
211 if viewport_changed {
212 debug!(target: "viewport_manager",
213 "Crosshair row set to: {}, adjusted viewport to: {:?}",
214 clamped_row, self.viewport_rows);
215 } else {
216 debug!(target: "viewport_manager",
217 "Crosshair row set to: {}", clamped_row);
218 }
219 }
220
221 pub fn set_crosshair_column(&mut self, col: usize) {
223 let total_columns = self.dataview.get_display_columns().len();
224
225 let clamped_col = col.min(total_columns.saturating_sub(1));
227 self.crosshair_col = clamped_col;
228
229 if self.viewport_lock {
231 debug!(target: "viewport_manager",
232 "Crosshair column set to: {} (viewport locked, no scroll adjustment)",
233 clamped_col);
234 return;
235 }
236
237 let terminal_width = self.terminal_width.saturating_sub(4); if self.set_current_column(clamped_col) {
240 debug!(target: "viewport_manager",
241 "Crosshair column set to: {} with viewport adjustment", clamped_col);
242 } else {
243 debug!(target: "viewport_manager",
244 "Crosshair column set to: {}", clamped_col);
245 }
246 }
247
248 #[must_use]
250 pub fn get_crosshair_col(&self) -> usize {
251 self.crosshair_col
252 }
253
254 #[must_use]
256 pub fn get_crosshair_row(&self) -> usize {
257 self.crosshair_row
258 }
259
260 #[must_use]
262 pub fn get_selected_row(&self) -> usize {
263 self.crosshair_row
264 }
265
266 #[must_use]
268 pub fn get_selected_column(&self) -> usize {
269 self.crosshair_col
270 }
271
272 #[must_use]
274 pub fn get_crosshair_position(&self) -> (usize, usize) {
275 (self.crosshair_row, self.crosshair_col)
276 }
277
278 #[must_use]
280 pub fn get_scroll_offset(&self) -> (usize, usize) {
281 (self.viewport_rows.start, self.viewport_cols.start)
282 }
283
284 pub fn set_scroll_offset(&mut self, row_offset: usize, col_offset: usize) {
286 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
287 let viewport_width = self.viewport_cols.end - self.viewport_cols.start;
288
289 self.viewport_rows = row_offset..(row_offset + viewport_height);
291 self.viewport_cols = col_offset..(col_offset + viewport_width);
292
293 if self.crosshair_row < self.viewport_rows.start {
295 self.crosshair_row = self.viewport_rows.start;
296 } else if self.crosshair_row >= self.viewport_rows.end {
297 self.crosshair_row = self.viewport_rows.end.saturating_sub(1);
298 }
299
300 if self.crosshair_col < self.viewport_cols.start {
301 self.crosshair_col = self.viewport_cols.start;
302 } else if self.crosshair_col >= self.viewport_cols.end {
303 self.crosshair_col = self.viewport_cols.end.saturating_sub(1);
304 }
305
306 self.cache_dirty = true;
307 }
308
309 #[must_use]
312 pub fn get_crosshair_viewport_position(&self) -> Option<(usize, usize)> {
313 if self.crosshair_row < self.viewport_rows.start
316 || self.crosshair_row >= self.viewport_rows.end
317 {
318 return None;
319 }
320
321 let pinned_count = self.dataview.get_pinned_columns().len();
323
324 if self.crosshair_col < pinned_count {
326 return Some((
327 self.crosshair_row - self.viewport_rows.start,
328 self.crosshair_col, ));
330 }
331
332 let scrollable_col = self.crosshair_col - pinned_count;
335 if scrollable_col >= self.viewport_cols.start && scrollable_col < self.viewport_cols.end {
336 let visual_col_in_viewport = pinned_count + (scrollable_col - self.viewport_cols.start);
339 return Some((
340 self.crosshair_row - self.viewport_rows.start,
341 visual_col_in_viewport,
342 ));
343 }
344
345 None
346 }
347
348 pub fn navigate_row_up(&mut self) -> RowNavigationResult {
350 let total_rows = self.dataview.row_count();
351
352 if self.viewport_lock {
354 debug!(target: "viewport_manager",
355 "navigate_row_up: Viewport locked, crosshair={}, viewport={:?}",
356 self.crosshair_row, self.viewport_rows);
357 if self.crosshair_row > self.viewport_rows.start {
359 self.crosshair_row -= 1;
360 return RowNavigationResult {
361 row_position: self.crosshair_row,
362 row_scroll_offset: self.viewport_rows.start,
363 description: "Moved within locked viewport".to_string(),
364 viewport_changed: false,
365 };
366 }
367 return RowNavigationResult {
369 row_position: self.crosshair_row,
370 row_scroll_offset: self.viewport_rows.start,
371 description: "Moved within locked viewport".to_string(),
372 viewport_changed: false,
373 };
374 }
375
376 if self.cursor_lock {
378 if let Some(lock_position) = self.cursor_lock_position {
379 if self.viewport_rows.start == 0 {
381 return RowNavigationResult {
383 row_position: self.crosshair_row,
384 row_scroll_offset: self.viewport_rows.start,
385 description: "At top of data".to_string(),
386 viewport_changed: false,
387 };
388 }
389
390 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
391 let new_viewport_start = self.viewport_rows.start.saturating_sub(1);
392
393 self.viewport_rows =
395 new_viewport_start..(new_viewport_start + viewport_height).min(total_rows);
396
397 self.crosshair_row = (self.viewport_rows.start + lock_position)
399 .min(self.viewport_rows.end.saturating_sub(1));
400
401 return RowNavigationResult {
402 row_position: self.crosshair_row,
403 row_scroll_offset: self.viewport_rows.start,
404 description: format!(
405 "Scrolled up (locked at viewport row {})",
406 lock_position + 1
407 ),
408 viewport_changed: true,
409 };
410 }
411 }
412
413 if self.crosshair_row == 0 {
416 return RowNavigationResult {
418 row_position: 0,
419 row_scroll_offset: self.viewport_rows.start,
420 description: "Already at first row".to_string(),
421 viewport_changed: false,
422 };
423 }
424
425 let new_row = self.crosshair_row - 1;
426 self.crosshair_row = new_row;
427
428 let viewport_changed = if new_row < self.viewport_rows.start {
430 self.viewport_rows = new_row..self.viewport_rows.end.saturating_sub(1);
431 true
432 } else {
433 false
434 };
435
436 RowNavigationResult {
437 row_position: new_row,
438 row_scroll_offset: self.viewport_rows.start,
439 description: format!("Move to row {}", new_row + 1),
440 viewport_changed,
441 }
442 }
443
444 pub fn navigate_row_down(&mut self) -> RowNavigationResult {
446 let total_rows = self.dataview.row_count();
447
448 if self.viewport_lock {
450 debug!(target: "viewport_manager",
451 "navigate_row_down: Viewport locked, crosshair={}, viewport={:?}",
452 self.crosshair_row, self.viewport_rows);
453 if self.crosshair_row < self.viewport_rows.end - 1
455 && self.crosshair_row < total_rows - 1
456 {
457 self.crosshair_row += 1;
458 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 return RowNavigationResult {
467 row_position: self.crosshair_row,
468 row_scroll_offset: self.viewport_rows.start,
469 description: "Moved within locked viewport".to_string(),
470 viewport_changed: false,
471 };
472 }
473
474 if self.cursor_lock {
476 if let Some(lock_position) = self.cursor_lock_position {
477 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
479 let new_viewport_start =
480 (self.viewport_rows.start + 1).min(total_rows.saturating_sub(viewport_height));
481
482 if new_viewport_start == self.viewport_rows.start {
483 return RowNavigationResult {
485 row_position: self.crosshair_row,
486 row_scroll_offset: self.viewport_rows.start,
487 description: "At bottom of data".to_string(),
488 viewport_changed: false,
489 };
490 }
491
492 self.viewport_rows =
494 new_viewport_start..(new_viewport_start + viewport_height).min(total_rows);
495
496 self.crosshair_row = (self.viewport_rows.start + lock_position)
498 .min(self.viewport_rows.end.saturating_sub(1));
499
500 return RowNavigationResult {
501 row_position: self.crosshair_row,
502 row_scroll_offset: self.viewport_rows.start,
503 description: format!(
504 "Scrolled down (locked at viewport row {})",
505 lock_position + 1
506 ),
507 viewport_changed: true,
508 };
509 }
510 }
511
512 if self.crosshair_row + 1 >= total_rows {
515 let last_row = total_rows.saturating_sub(1);
517 return RowNavigationResult {
518 row_position: last_row,
519 row_scroll_offset: self.viewport_rows.start,
520 description: "Already at last row".to_string(),
521 viewport_changed: false,
522 };
523 }
524
525 let new_row = self.crosshair_row + 1;
526 self.crosshair_row = new_row;
527
528 let viewport_changed = if new_row >= self.viewport_rows.end {
531 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
533 self.viewport_rows = (new_row + 1).saturating_sub(viewport_height)..(new_row + 1);
534 true
535 } else {
536 false
537 };
538
539 RowNavigationResult {
540 row_position: new_row,
541 row_scroll_offset: self.viewport_rows.start,
542 description: format!("Move to row {}", new_row + 1),
543 viewport_changed,
544 }
545 }
546
547 #[must_use]
549 pub fn new(dataview: Arc<DataView>) -> Self {
550 let display_columns = dataview.get_display_columns();
552 let visible_col_count = display_columns.len();
553 let total_col_count = dataview.source().column_count(); let total_rows = dataview.row_count();
555
556 let initial_viewport_cols = if visible_col_count > 0 {
558 0..visible_col_count.min(20) } else {
560 0..0
561 };
562
563 let default_visible_rows = 50usize; let initial_viewport_rows = if total_rows > 0 {
567 0..total_rows.min(default_visible_rows)
568 } else {
569 0..0
570 };
571
572 Self {
573 dataview,
574 viewport_rows: initial_viewport_rows,
575 viewport_cols: initial_viewport_cols,
576 terminal_width: 80,
577 terminal_height: 24,
578 width_calculator: ColumnWidthCalculator::new(),
579 visible_row_cache: Vec::new(),
580 cache_signature: 0,
581 cache_dirty: true,
582 crosshair_row: 0,
583 crosshair_col: 0,
584 cursor_lock: false,
585 cursor_lock_position: None,
586 viewport_lock: false,
587 viewport_lock_boundaries: None,
588 }
589 }
590
591 pub fn set_dataview(&mut self, dataview: Arc<DataView>) {
593 self.dataview = dataview;
594 self.invalidate_cache();
595 }
596
597 pub fn reset_crosshair(&mut self) {
599 self.crosshair_row = 0;
600 self.crosshair_col = 0;
601 self.cursor_lock = false;
602 self.cursor_lock_position = None;
603 }
604
605 #[must_use]
607 pub fn get_packing_mode(&self) -> ColumnPackingMode {
608 self.width_calculator.get_packing_mode()
609 }
610
611 pub fn set_packing_mode(&mut self, mode: ColumnPackingMode) {
613 self.width_calculator.set_packing_mode(mode);
614 self.invalidate_cache();
615 }
616
617 pub fn cycle_packing_mode(&mut self) -> ColumnPackingMode {
619 self.width_calculator.cycle_packing_mode();
620 self.invalidate_cache();
621 self.width_calculator.get_packing_mode()
622 }
623
624 pub fn set_viewport(&mut self, row_offset: usize, col_offset: usize, width: u16, height: u16) {
626 let new_rows = row_offset
627 ..row_offset
628 .saturating_add(height as usize)
629 .min(self.dataview.row_count());
630
631 let display_columns = self.dataview.get_display_columns();
634 let visual_column_count = display_columns.len();
635
636 let columns_that_fit = self.calculate_columns_that_fit(col_offset, width);
638 let new_cols = col_offset
639 ..col_offset
640 .saturating_add(columns_that_fit)
641 .min(visual_column_count);
642
643 if new_rows != self.viewport_rows || new_cols != self.viewport_cols {
645 self.viewport_rows = new_rows;
646 self.viewport_cols = new_cols;
647 self.terminal_width = width;
648 self.terminal_height = height;
649 self.cache_dirty = true;
650 }
651 }
652
653 pub fn update_terminal_size(&mut self, terminal_width: u16, terminal_height: u16) -> usize {
656 let visible_rows = (terminal_height as usize).max(10);
659
660 debug!(target: "viewport_manager",
661 "update_terminal_size: terminal_height={}, calculated visible_rows={}",
662 terminal_height, visible_rows
663 );
664
665 let old_viewport = self.viewport_rows.clone();
666
667 self.terminal_width = terminal_width;
669 self.terminal_height = terminal_height;
670
671 let total_rows = self.dataview.row_count();
674
675 let viewport_size = self.viewport_rows.end - self.viewport_rows.start;
677 if viewport_size != visible_rows && total_rows > 0 {
678 if self.crosshair_row < self.viewport_rows.start {
681 self.viewport_rows =
683 self.crosshair_row..(self.crosshair_row + visible_rows).min(total_rows);
684 } else if self.crosshair_row >= self.viewport_rows.start + visible_rows {
685 let start = self.crosshair_row.saturating_sub(visible_rows - 1);
687 self.viewport_rows = start..(start + visible_rows).min(total_rows);
688 } else {
689 self.viewport_rows = self.viewport_rows.start
691 ..(self.viewport_rows.start + visible_rows).min(total_rows);
692 }
693 }
694
695 let visible_column_count = self.dataview.get_display_columns().len();
698 if visible_column_count > 0 {
699 let columns_that_fit = self.calculate_columns_that_fit(
703 self.viewport_cols.start,
704 terminal_width.saturating_sub(2), );
706
707 let new_col_viewport_end = self
708 .viewport_cols
709 .start
710 .saturating_add(columns_that_fit)
711 .min(visible_column_count);
712
713 let old_col_viewport = self.viewport_cols.clone();
714 self.viewport_cols = self.viewport_cols.start..new_col_viewport_end;
715
716 if old_col_viewport != self.viewport_cols {
717 debug!(target: "viewport_manager",
718 "update_terminal_size - column viewport changed from {:?} to {:?}, terminal_width={}",
719 old_col_viewport, self.viewport_cols, terminal_width
720 );
721 self.cache_dirty = true;
722 }
723 }
724
725 if old_viewport != self.viewport_rows {
726 debug!(target: "navigation",
727 "ViewportManager::update_terminal_size - viewport changed from {:?} to {:?}, crosshair={}, visible_rows={}",
728 old_viewport, self.viewport_rows, self.crosshair_row, visible_rows
729 );
730 }
731
732 visible_rows
733 }
734
735 pub fn scroll_by(&mut self, row_delta: isize, col_delta: isize) {
737 let new_row_start = (self.viewport_rows.start as isize + row_delta).max(0) as usize;
738 let new_col_start = (self.viewport_cols.start as isize + col_delta).max(0) as usize;
739
740 self.set_viewport(
741 new_row_start,
742 new_col_start,
743 self.terminal_width,
744 self.terminal_height,
745 );
746 }
747
748 pub fn get_column_widths(&mut self) -> &[u16] {
750 self.width_calculator
751 .get_all_column_widths(&self.dataview, &self.viewport_rows)
752 }
753
754 pub fn get_column_width(&mut self, col_idx: usize) -> u16 {
756 self.width_calculator
757 .get_column_width(&self.dataview, &self.viewport_rows, col_idx)
758 }
759
760 #[must_use]
762 pub fn get_visible_rows(&self) -> Vec<DataRow> {
763 let mut rows = Vec::with_capacity(self.viewport_rows.len());
764
765 for row_idx in self.viewport_rows.clone() {
766 if let Some(row) = self.dataview.get_row(row_idx) {
767 rows.push(row);
768 }
769 }
770
771 rows
772 }
773
774 #[must_use]
776 pub fn get_visible_row(&self, viewport_row: usize) -> Option<DataRow> {
777 let absolute_row = self.viewport_rows.start + viewport_row;
778 if absolute_row < self.viewport_rows.end {
779 self.dataview.get_row(absolute_row)
780 } else {
781 None
782 }
783 }
784
785 #[must_use]
787 pub fn get_visible_columns(&self) -> Vec<String> {
788 let display_column_names = self.dataview.get_display_column_names();
790
791 let mut visible = Vec::new();
793 for col_idx in self.viewport_cols.clone() {
794 if col_idx < display_column_names.len() {
795 visible.push(display_column_names[col_idx].clone());
796 }
797 }
798
799 visible
800 }
801
802 #[must_use]
804 pub fn viewport_rows(&self) -> Range<usize> {
805 self.viewport_rows.clone()
806 }
807
808 #[must_use]
810 pub fn viewport_cols(&self) -> Range<usize> {
811 self.viewport_cols.clone()
812 }
813
814 #[must_use]
816 pub fn is_row_visible(&self, row_idx: usize) -> bool {
817 self.viewport_rows.contains(&row_idx)
818 }
819
820 #[must_use]
822 pub fn is_column_visible(&self, col_idx: usize) -> bool {
823 self.viewport_cols.contains(&col_idx)
824 }
825
826 #[must_use]
828 pub fn total_rows(&self) -> usize {
829 self.dataview.row_count()
830 }
831
832 #[must_use]
834 pub fn total_columns(&self) -> usize {
835 self.dataview.column_count()
836 }
837
838 #[must_use]
840 pub fn get_terminal_width(&self) -> u16 {
841 self.terminal_width
842 }
843
844 #[must_use]
846 pub fn get_terminal_height(&self) -> usize {
847 self.terminal_height as usize
848 }
849
850 pub fn invalidate_cache(&mut self) {
852 self.cache_dirty = true;
853 self.width_calculator.mark_dirty();
854 }
855
856 pub fn calculate_visible_column_indices(&mut self, available_width: u16) -> Vec<usize> {
860 let display_columns = self.dataview.get_display_columns();
864 let total_visual_columns = display_columns.len();
865
866 if total_visual_columns == 0 {
867 return Vec::new();
868 }
869
870 let pinned_columns = self.dataview.get_pinned_columns();
872 let pinned_count = pinned_columns.len();
873
874 let mut used_width = 0u16;
875 let separator_width = 1u16;
876 let mut result = Vec::new();
877
878 tracing::debug!("[PIN_DEBUG] === calculate_visible_column_indices ===");
879 tracing::debug!(
880 "[PIN_DEBUG] available_width={}, total_visual_columns={}",
881 available_width,
882 total_visual_columns
883 );
884 tracing::debug!(
885 "[PIN_DEBUG] pinned_columns={:?} (count={})",
886 pinned_columns,
887 pinned_count
888 );
889 tracing::debug!("[PIN_DEBUG] viewport_cols={:?}", self.viewport_cols);
890 tracing::debug!("[PIN_DEBUG] display_columns={:?}", display_columns);
891
892 debug!(target: "viewport_manager",
893 "calculate_visible_column_indices: available_width={}, total_visual_columns={}, pinned_count={}, viewport_start={}",
894 available_width, total_visual_columns, pinned_count, self.viewport_cols.start);
895
896 for visual_idx in 0..pinned_count {
898 if visual_idx >= display_columns.len() {
899 break;
900 }
901
902 let datatable_idx = display_columns[visual_idx];
903 let width = self.width_calculator.get_column_width(
904 &self.dataview,
905 &self.viewport_rows,
906 datatable_idx,
907 );
908
909 used_width += width + separator_width;
911 result.push(datatable_idx);
912 tracing::debug!(
913 "[PIN_DEBUG] Added pinned column: visual_idx={}, datatable_idx={}, width={}",
914 visual_idx,
915 datatable_idx,
916 width
917 );
918 }
919
920 let scrollable_start = self.viewport_cols.start;
923 let visual_start = scrollable_start + pinned_count;
924
925 tracing::debug!(
926 "[PIN_DEBUG] viewport_cols.start={} is SCROLLABLE index",
927 self.viewport_cols.start
928 );
929 tracing::debug!(
930 "[PIN_DEBUG] visual_start={} (scrollable_start {} + pinned_count {})",
931 visual_start,
932 scrollable_start,
933 pinned_count
934 );
935
936 let visual_start = visual_start.min(total_visual_columns);
937
938 for visual_idx in visual_start..total_visual_columns {
940 let datatable_idx = display_columns[visual_idx];
942
943 let width = self.width_calculator.get_column_width(
944 &self.dataview,
945 &self.viewport_rows,
946 datatable_idx,
947 );
948
949 if used_width + width + separator_width <= available_width {
950 used_width += width + separator_width;
951 result.push(datatable_idx);
952 tracing::debug!("[PIN_DEBUG] Added scrollable column: visual_idx={}, datatable_idx={}, width={}", visual_idx, datatable_idx, width);
953 } else {
954 tracing::debug!(
955 "[PIN_DEBUG] Stopped at visual_idx={} - would exceed width",
956 visual_idx
957 );
958 break;
959 }
960 }
961
962 if result.is_empty() && total_visual_columns > 0 {
965 result.push(display_columns[0]);
966 }
967
968 tracing::debug!("[PIN_DEBUG] Final result: {:?}", result);
969 tracing::debug!("[PIN_DEBUG] === End calculate_visible_column_indices ===");
970 debug!(target: "viewport_manager",
971 "calculate_visible_column_indices RESULT: pinned={}, viewport_start={}, visual_start={} -> DataTable indices {:?}",
972 pinned_count, self.viewport_cols.start, visual_start, result);
973
974 result
975 }
976
977 pub fn calculate_columns_that_fit(&mut self, start_col: usize, available_width: u16) -> usize {
980 let mut used_width = 0u16;
983 let mut column_count = 0usize;
984 let separator_width = 1u16;
985
986 for col_idx in start_col..self.dataview.column_count() {
987 let width = self.width_calculator.get_column_width(
988 &self.dataview,
989 &self.viewport_rows,
990 col_idx,
991 );
992 if used_width + width + separator_width <= available_width {
993 used_width += width + separator_width;
994 column_count += 1;
995 } else {
996 break;
997 }
998 }
999
1000 column_count.max(1) }
1002
1003 pub fn get_column_widths_for(&mut self, column_indices: &[usize]) -> Vec<u16> {
1006 column_indices
1007 .iter()
1008 .map(|&idx| {
1009 self.width_calculator
1010 .get_column_width(&self.dataview, &self.viewport_rows, idx)
1011 })
1012 .collect()
1013 }
1014
1015 pub fn update_column_viewport(&mut self, start_col: usize, available_width: u16) {
1018 let col_count = self.calculate_columns_that_fit(start_col, available_width);
1019 let end_col = (start_col + col_count).min(self.dataview.column_count());
1020
1021 if self.viewport_cols.start != start_col || self.viewport_cols.end != end_col {
1022 self.viewport_cols = start_col..end_col;
1023 self.cache_dirty = true;
1024 }
1025 }
1026
1027 #[must_use]
1029 pub fn dataview(&self) -> &DataView {
1030 &self.dataview
1031 }
1032
1033 #[must_use]
1036 pub fn clone_dataview(&self) -> DataView {
1037 (*self.dataview).clone()
1038 }
1039
1040 pub fn calculate_optimal_offset_for_last_column(&mut self, available_width: u16) -> usize {
1044 let display_columns = self.dataview.get_display_columns();
1048 if display_columns.is_empty() {
1049 return 0;
1050 }
1051
1052 let pinned = self.dataview.get_pinned_columns();
1053 let pinned_count = pinned.len();
1054
1055 let mut pinned_width = 0u16;
1057 let separator_width = 1u16;
1058 for &col_idx in pinned {
1059 let width = self.width_calculator.get_column_width(
1060 &self.dataview,
1061 &self.viewport_rows,
1062 col_idx,
1063 );
1064 pinned_width += width + separator_width;
1065 }
1066
1067 let available_for_scrollable = available_width.saturating_sub(pinned_width);
1069
1070 let scrollable_columns: Vec<usize> = display_columns
1072 .iter()
1073 .filter(|&&col| !pinned.contains(&col))
1074 .copied()
1075 .collect();
1076
1077 if scrollable_columns.is_empty() {
1078 return 0;
1079 }
1080
1081 let last_col_idx = *scrollable_columns.last().unwrap();
1083 let last_col_width = self.width_calculator.get_column_width(
1084 &self.dataview,
1085 &self.viewport_rows,
1086 last_col_idx,
1087 );
1088
1089 tracing::debug!(
1090 "Starting calculation: last_col_idx={}, width={}w, available={}w, scrollable_cols={}",
1091 last_col_idx,
1092 last_col_width,
1093 available_for_scrollable,
1094 scrollable_columns.len()
1095 );
1096
1097 let mut accumulated_width = last_col_width + separator_width;
1098 let mut best_offset = scrollable_columns.len() - 1; for (idx, &col_idx) in scrollable_columns.iter().enumerate().rev().skip(1) {
1102 let width = self.width_calculator.get_column_width(
1103 &self.dataview,
1104 &self.viewport_rows,
1105 col_idx,
1106 );
1107
1108 let width_with_separator = width + separator_width;
1109
1110 if accumulated_width + width_with_separator <= available_for_scrollable {
1111 accumulated_width += width_with_separator;
1113 best_offset = idx; tracing::trace!(
1115 "Column {} (idx {}) fits ({}w), accumulated={}w, new offset={}",
1116 col_idx,
1117 idx,
1118 width,
1119 accumulated_width,
1120 best_offset
1121 );
1122 } else {
1123 best_offset = idx + 1;
1126 tracing::trace!(
1127 "Column {} doesn't fit ({}w would make {}w total), stopping at offset {}",
1128 col_idx,
1129 width,
1130 accumulated_width + width_with_separator,
1131 best_offset
1132 );
1133 break;
1134 }
1135 }
1136
1137 let mut test_width = 0u16;
1143 let mut can_see_last = false;
1144 for idx in best_offset..scrollable_columns.len() {
1145 let col_idx = scrollable_columns[idx];
1146 let width = self.width_calculator.get_column_width(
1147 &self.dataview,
1148 &self.viewport_rows,
1149 col_idx,
1150 );
1151 test_width += width + separator_width;
1152
1153 if test_width > available_for_scrollable {
1154 tracing::warn!(
1157 "Offset {} doesn't show last column! Need {}w but have {}w",
1158 best_offset,
1159 test_width,
1160 available_for_scrollable
1161 );
1162 best_offset += 1;
1164 can_see_last = false;
1165 break;
1166 }
1167 if idx == scrollable_columns.len() - 1 {
1168 can_see_last = true;
1169 }
1170 }
1171
1172 while !can_see_last && best_offset < scrollable_columns.len() {
1174 test_width = 0;
1175 for idx in best_offset..scrollable_columns.len() {
1176 let col_idx = scrollable_columns[idx];
1177 let width = self.width_calculator.get_column_width(
1178 &self.dataview,
1179 &self.viewport_rows,
1180 col_idx,
1181 );
1182 test_width += width + separator_width;
1183
1184 if test_width > available_for_scrollable {
1185 best_offset += 1;
1186 break;
1187 }
1188 if idx == scrollable_columns.len() - 1 {
1189 can_see_last = true;
1190 }
1191 }
1192 }
1193
1194 tracing::debug!(
1196 "Final offset for last column: scrollable_offset={}, fits {} columns, last col width: {}w, verified last col visible: {}",
1197 best_offset,
1198 scrollable_columns.len() - best_offset,
1199 last_col_width,
1200 can_see_last
1201 );
1202
1203 best_offset
1204 }
1205
1206 pub fn debug_dump(&mut self, available_width: u16) -> String {
1208 let mut output = String::new();
1211 output.push_str("========== VIEWPORT MANAGER DEBUG ==========\n");
1212
1213 let total_cols = self.dataview.column_count();
1214 let pinned = self.dataview.get_pinned_columns();
1215 let pinned_count = pinned.len();
1216
1217 output.push_str(&format!("Total columns: {total_cols}\n"));
1218 output.push_str(&format!("Pinned columns: {pinned:?}\n"));
1219 output.push_str(&format!("Available width: {available_width}w\n"));
1220 output.push_str(&format!("Current viewport: {:?}\n", self.viewport_cols));
1221 output.push_str(&format!(
1222 "Packing mode: {} (Alt+S to cycle)\n",
1223 self.width_calculator.get_packing_mode().display_name()
1224 ));
1225 output.push('\n');
1226
1227 output.push_str("=== COLUMN WIDTH CALCULATIONS ===\n");
1229 output.push_str(&format!(
1230 "Mode: {}\n",
1231 self.width_calculator.get_packing_mode().display_name()
1232 ));
1233
1234 let debug_info = self.width_calculator.get_debug_info();
1236 if !debug_info.is_empty() {
1237 output.push_str("Visible columns in viewport:\n");
1238
1239 let mut visible_count = 0;
1241 for col_idx in self.viewport_cols.clone() {
1242 if col_idx < debug_info.len() {
1243 let (ref col_name, header_width, max_data_width, final_width, sample_count) =
1244 debug_info[col_idx];
1245
1246 let reason = match self.width_calculator.get_packing_mode() {
1248 ColumnPackingMode::DataFocus => {
1249 if max_data_width <= 3 {
1250 format!("Ultra aggressive (data:{max_data_width}≤3 chars)")
1251 } else if max_data_width <= 10 && header_width > max_data_width * 2 {
1252 format!(
1253 "Aggressive truncate (data:{}≤10, header:{}>{} )",
1254 max_data_width,
1255 header_width,
1256 max_data_width * 2
1257 )
1258 } else if final_width == MAX_COL_WIDTH_DATA_FOCUS {
1259 "Max width reached".to_string()
1260 } else {
1261 "Data-based width".to_string()
1262 }
1263 }
1264 ColumnPackingMode::HeaderFocus => {
1265 if final_width == header_width + COLUMN_PADDING {
1266 "Full header shown".to_string()
1267 } else if final_width == MAX_COL_WIDTH {
1268 "Max width reached".to_string()
1269 } else {
1270 "Header priority".to_string()
1271 }
1272 }
1273 ColumnPackingMode::Balanced => {
1274 if header_width > max_data_width && final_width < header_width {
1275 "Header constrained by ratio".to_string()
1276 } else {
1277 "Balanced".to_string()
1278 }
1279 }
1280 };
1281
1282 output.push_str(&format!(
1283 " [{col_idx}] \"{col_name}\":\n Header: {header_width}w, Data: {max_data_width}w → Final: {final_width}w ({reason}, {sample_count} samples)\n"
1284 ));
1285
1286 visible_count += 1;
1287
1288 if visible_count >= 10 {
1290 let remaining = self.viewport_cols.end - self.viewport_cols.start - 10;
1291 if remaining > 0 {
1292 output.push_str(&format!(" ... and {remaining} more columns\n"));
1293 }
1294 break;
1295 }
1296 }
1297 }
1298 }
1299
1300 output.push('\n');
1301
1302 output.push_str("Column width summary (all columns):\n");
1304 let all_widths = self
1305 .width_calculator
1306 .get_all_column_widths(&self.dataview, &self.viewport_rows);
1307 for (idx, &width) in all_widths.iter().enumerate() {
1308 if idx >= 20 && idx < total_cols - 10 {
1309 if idx == 20 {
1310 output.push_str(" ... (showing only first 20 and last 10)\n");
1311 }
1312 continue;
1313 }
1314 output.push_str(&format!(" [{idx}] {width}w\n"));
1315 }
1316 output.push('\n');
1317
1318 output.push_str("=== OPTIMAL OFFSET CALCULATION ===\n");
1320 let last_col_idx = total_cols - 1;
1321 let last_col_width = self.width_calculator.get_column_width(
1322 &self.dataview,
1323 &self.viewport_rows,
1324 last_col_idx,
1325 );
1326
1327 let separator_width = 1u16;
1329 let mut pinned_width = 0u16;
1330 for &col_idx in pinned {
1331 let width = self.width_calculator.get_column_width(
1332 &self.dataview,
1333 &self.viewport_rows,
1334 col_idx,
1335 );
1336 pinned_width += width + separator_width;
1337 }
1338 let available_for_scrollable = available_width.saturating_sub(pinned_width);
1339
1340 output.push_str(&format!(
1341 "Last column: {last_col_idx} (width: {last_col_width}w)\n"
1342 ));
1343 output.push_str(&format!("Pinned width: {pinned_width}w\n"));
1344 output.push_str(&format!(
1345 "Available for scrollable: {available_for_scrollable}w\n"
1346 ));
1347 output.push('\n');
1348
1349 let mut accumulated_width = last_col_width + separator_width;
1351 let mut best_offset = last_col_idx;
1352
1353 output.push_str("Backtracking from last column:\n");
1354 output.push_str(&format!(
1355 " Start: column {last_col_idx} = {last_col_width}w (accumulated: {accumulated_width}w)\n"
1356 ));
1357
1358 for col_idx in (pinned_count..last_col_idx).rev() {
1359 let width = self.width_calculator.get_column_width(
1360 &self.dataview,
1361 &self.viewport_rows,
1362 col_idx,
1363 );
1364 let width_with_sep = width + separator_width;
1365
1366 if accumulated_width + width_with_sep <= available_for_scrollable {
1367 accumulated_width += width_with_sep;
1368 best_offset = col_idx;
1369 output.push_str(&format!(
1370 " Column {col_idx} fits: {width}w (accumulated: {accumulated_width}w, offset: {best_offset})\n"
1371 ));
1372 } else {
1373 output.push_str(&format!(
1374 " Column {} doesn't fit: {}w (would make {}w > {}w)\n",
1375 col_idx,
1376 width,
1377 accumulated_width + width_with_sep,
1378 available_for_scrollable
1379 ));
1380 best_offset = col_idx + 1;
1381 break;
1382 }
1383 }
1384
1385 output.push_str(&format!("\nCalculated offset: {best_offset} (absolute)\n"));
1386
1387 output.push_str("\n=== VERIFICATION ===\n");
1389 let mut verify_width = 0u16;
1390 let mut can_show_last = true;
1391
1392 for test_idx in best_offset..=last_col_idx {
1393 let width = self.width_calculator.get_column_width(
1394 &self.dataview,
1395 &self.viewport_rows,
1396 test_idx,
1397 );
1398 verify_width += width + separator_width;
1399
1400 output.push_str(&format!(
1401 " Column {test_idx}: {width}w (running total: {verify_width}w)\n"
1402 ));
1403
1404 if verify_width > available_for_scrollable {
1405 output.push_str(&format!(
1406 " ❌ EXCEEDS LIMIT! {verify_width}w > {available_for_scrollable}w\n"
1407 ));
1408 if test_idx == last_col_idx {
1409 can_show_last = false;
1410 output.push_str(" ❌ LAST COLUMN NOT VISIBLE!\n");
1411 }
1412 break;
1413 }
1414
1415 if test_idx == last_col_idx {
1416 output.push_str(" ✅ LAST COLUMN VISIBLE!\n");
1417 }
1418 }
1419
1420 output.push_str(&format!(
1421 "\nVerification result: last column visible = {can_show_last}\n"
1422 ));
1423
1424 output.push_str("\n=== CURRENT VIEWPORT RESULT ===\n");
1426 let visible_indices = self.calculate_visible_column_indices(available_width);
1427 output.push_str(&format!("Visible columns: {visible_indices:?}\n"));
1428 output.push_str(&format!(
1429 "Last visible column: {}\n",
1430 visible_indices.last().copied().unwrap_or(0)
1431 ));
1432 output.push_str(&format!(
1433 "Shows last column ({}): {}\n",
1434 last_col_idx,
1435 visible_indices.contains(&last_col_idx)
1436 ));
1437
1438 output.push_str("============================================\n");
1439 output
1440 }
1441
1442 #[must_use]
1445 pub fn get_column_names_ordered(&self) -> Vec<String> {
1446 self.dataview.column_names()
1447 }
1448
1449 pub fn get_visible_columns_info(
1452 &mut self,
1453 available_width: u16,
1454 ) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
1455 debug!(target: "viewport_manager",
1456 "get_visible_columns_info CALLED with width={}, current_viewport={:?}",
1457 available_width, self.viewport_cols);
1458
1459 let viewport_indices = self.calculate_visible_column_indices(available_width);
1461
1462 let display_order = self.dataview.get_display_columns();
1464 let mut visible_indices = Vec::new();
1465
1466 for &col_idx in &display_order {
1468 if viewport_indices.contains(&col_idx) {
1469 visible_indices.push(col_idx);
1470 }
1471 }
1472
1473 let pinned_columns = self.dataview.get_pinned_columns();
1475
1476 let mut pinned_visible = Vec::new();
1478 let mut scrollable_visible = Vec::new();
1479
1480 for &idx in &visible_indices {
1481 if pinned_columns.contains(&idx) {
1482 pinned_visible.push(idx);
1483 } else {
1484 scrollable_visible.push(idx);
1485 }
1486 }
1487
1488 debug!(target: "viewport_manager",
1489 "get_visible_columns_info: viewport={:?} -> ordered={:?} ({} pinned, {} scrollable)",
1490 viewport_indices, visible_indices, pinned_visible.len(), scrollable_visible.len());
1491
1492 debug!(target: "viewport_manager",
1493 "RENDERER DEBUG: viewport_indices={:?}, display_order={:?}, visible_indices={:?}",
1494 viewport_indices, display_order, visible_indices);
1495
1496 (visible_indices, pinned_visible, scrollable_visible)
1497 }
1498
1499 pub fn calculate_column_x_positions(&mut self, available_width: u16) -> (Vec<usize>, Vec<u16>) {
1502 let visible_indices = self.calculate_visible_column_indices(available_width);
1503 let mut x_positions = Vec::new();
1504 let mut current_x = 0u16;
1505 let separator_width = 1u16;
1506
1507 for &col_idx in &visible_indices {
1508 x_positions.push(current_x);
1509 let width = self.width_calculator.get_column_width(
1510 &self.dataview,
1511 &self.viewport_rows,
1512 col_idx,
1513 );
1514 current_x += width + separator_width;
1515 }
1516
1517 (visible_indices, x_positions)
1518 }
1519
1520 pub fn get_column_x_position(&mut self, column: usize, available_width: u16) -> Option<u16> {
1522 let (indices, positions) = self.calculate_column_x_positions(available_width);
1523 indices
1524 .iter()
1525 .position(|&idx| idx == column)
1526 .and_then(|pos| positions.get(pos).copied())
1527 }
1528
1529 pub fn calculate_visible_column_indices_ordered(&mut self, available_width: u16) -> Vec<usize> {
1531 let ordered_columns = self.dataview.get_display_columns();
1535 let mut visible_indices = Vec::new();
1536 let mut used_width = 0u16;
1537 let separator_width = 1u16;
1538
1539 tracing::trace!(
1540 "ViewportManager: Starting ordered column layout. Available width: {}w, DataView order: {:?}",
1541 available_width,
1542 ordered_columns
1543 );
1544
1545 for &col_idx in &ordered_columns {
1547 let width = self.width_calculator.get_column_width(
1548 &self.dataview,
1549 &self.viewport_rows,
1550 col_idx,
1551 );
1552
1553 if used_width + width + separator_width <= available_width {
1554 visible_indices.push(col_idx);
1555 used_width += width + separator_width;
1556 tracing::trace!(
1557 "Added column {} in DataView order: {}w (total used: {}w)",
1558 col_idx,
1559 width,
1560 used_width
1561 );
1562 } else {
1563 tracing::trace!(
1564 "Skipped column {} ({}w) - would exceed available width",
1565 col_idx,
1566 width
1567 );
1568 break; }
1570 }
1571
1572 tracing::trace!(
1573 "Final ordered layout: {} columns visible {:?}, {}w used of {}w",
1574 visible_indices.len(),
1575 visible_indices,
1576 used_width,
1577 available_width
1578 );
1579
1580 visible_indices
1581 }
1582
1583 pub fn get_display_position_for_datatable_column(
1586 &mut self,
1587 datatable_column: usize,
1588 available_width: u16,
1589 ) -> Option<usize> {
1590 let visible_columns_info = self.get_visible_columns_info(available_width);
1591 let visible_indices = visible_columns_info.0;
1592
1593 let position = visible_indices
1595 .iter()
1596 .position(|&col| col == datatable_column);
1597
1598 debug!(target: "viewport_manager",
1599 "get_display_position_for_datatable_column: datatable_column={}, visible_indices={:?}, position={:?}",
1600 datatable_column, visible_indices, position);
1601
1602 position
1603 }
1604
1605 pub fn get_crosshair_column(
1610 &mut self,
1611 current_datatable_column: usize,
1612 available_width: u16,
1613 ) -> Option<usize> {
1614 let visible_columns_info = self.get_visible_columns_info(available_width);
1616 let visible_indices = visible_columns_info.0;
1617
1618 let position = visible_indices
1620 .iter()
1621 .position(|&col| col == current_datatable_column);
1622
1623 debug!(target: "viewport_manager",
1624 "CROSSHAIR: current_datatable_column={}, visible_indices={:?}, crosshair_position={:?}",
1625 current_datatable_column, visible_indices, position);
1626
1627 position
1628 }
1629
1630 pub fn get_visual_display(
1634 &mut self,
1635 available_width: u16,
1636 _row_indices: &[usize], ) -> (Vec<String>, Vec<Vec<String>>, Vec<u16>) {
1638 let row_indices: Vec<usize> = (self.viewport_rows.start..self.viewport_rows.end).collect();
1640
1641 debug!(target: "viewport_manager",
1642 "get_visual_display: Using viewport_rows {:?} -> row_indices: {:?} (first 5)",
1643 self.viewport_rows,
1644 row_indices.iter().take(5).collect::<Vec<_>>());
1645 let visible_column_indices = self.calculate_visible_column_indices(available_width);
1648
1649 tracing::debug!(
1650 "[RENDER_DEBUG] visible_column_indices from calculate: {:?}",
1651 visible_column_indices
1652 );
1653
1654 let all_headers = self.dataview.get_display_column_names();
1656 let display_columns = self.dataview.get_display_columns();
1657 let total_visual_columns = all_headers.len();
1658
1659 debug!(target: "viewport_manager",
1660 "get_visual_display: {} total visual columns, viewport: {:?}",
1661 total_visual_columns, self.viewport_cols);
1662
1663 let headers: Vec<String> = visible_column_indices
1665 .iter()
1666 .filter_map(|&dt_idx| {
1667 display_columns
1669 .iter()
1670 .position(|&x| x == dt_idx)
1671 .and_then(|visual_idx| all_headers.get(visual_idx).cloned())
1672 })
1673 .collect();
1674
1675 tracing::debug!("[RENDER_DEBUG] headers: {:?}", headers);
1676
1677 let visual_rows: Vec<Vec<String>> = row_indices
1680 .iter()
1681 .filter_map(|&display_row_idx| {
1682 let row_data = self.dataview.get_row_visual_values(display_row_idx);
1685 if let Some(ref full_row) = row_data {
1686 if !(5..19900).contains(&display_row_idx) {
1688 debug!(target: "viewport_manager",
1689 "DATAVIEW FETCH: display_row_idx {} -> data: {:?} (first 3 cols)",
1690 display_row_idx,
1691 full_row.iter().take(3).collect::<Vec<_>>());
1692 }
1693 }
1694 row_data.map(|full_row| {
1695 visible_column_indices
1697 .iter()
1698 .filter_map(|&dt_idx| {
1699 display_columns
1701 .iter()
1702 .position(|&x| x == dt_idx)
1703 .and_then(|visual_idx| full_row.get(visual_idx).cloned())
1704 })
1705 .collect()
1706 })
1707 })
1708 .collect();
1709
1710 let widths: Vec<u16> = visible_column_indices
1712 .iter()
1713 .map(|&dt_idx| {
1714 self.width_calculator
1715 .get_column_width(&self.dataview, &self.viewport_rows, dt_idx)
1716 })
1717 .collect();
1718
1719 debug!(target: "viewport_manager",
1720 "get_visual_display RESULT: {} headers, {} rows",
1721 headers.len(), visual_rows.len());
1722 if let Some(first_row) = visual_rows.first() {
1723 debug!(target: "viewport_manager",
1724 "Alignment check (FIRST ROW): {:?}",
1725 headers.iter().zip(first_row).take(5)
1726 .map(|(h, v)| format!("{h}: {v}")).collect::<Vec<_>>());
1727 }
1728 if let Some(last_row) = visual_rows.last() {
1729 debug!(target: "viewport_manager",
1730 "Alignment check (LAST ROW): {:?}",
1731 headers.iter().zip(last_row).take(5)
1732 .map(|(h, v)| format!("{h}: {v}")).collect::<Vec<_>>());
1733 }
1734
1735 (headers, visual_rows, widths)
1736 }
1737
1738 pub fn get_visible_column_headers(&self, visible_indices: &[usize]) -> Vec<String> {
1741 let mut headers = Vec::new();
1742
1743 let source = self.dataview.source();
1746 let all_column_names = source.column_names();
1747
1748 for &visual_idx in visible_indices {
1749 if visual_idx < all_column_names.len() {
1750 headers.push(all_column_names[visual_idx].clone());
1751 } else {
1752 headers.push(format!("Column_{visual_idx}"));
1754 }
1755 }
1756
1757 debug!(target: "viewport_manager",
1758 "get_visible_column_headers: indices={:?} -> headers={:?}",
1759 visible_indices, headers);
1760
1761 headers
1762 }
1763
1764 pub fn get_crosshair_column_for_display(
1767 &mut self,
1768 current_display_position: usize,
1769 available_width: u16,
1770 ) -> Option<usize> {
1771 let display_columns = self.dataview.get_display_columns();
1773
1774 if current_display_position >= display_columns.len() {
1776 debug!(target: "viewport_manager",
1777 "CROSSHAIR DISPLAY: display_position {} out of bounds (max {})",
1778 current_display_position, display_columns.len());
1779 return None;
1780 }
1781
1782 let datatable_column = display_columns[current_display_position];
1784
1785 let visible_columns_info = self.get_visible_columns_info(available_width);
1787 let visible_indices = visible_columns_info.0;
1788
1789 let position = visible_indices
1791 .iter()
1792 .position(|&col| col == datatable_column);
1793
1794 debug!(target: "viewport_manager",
1795 "CROSSHAIR DISPLAY: display_pos={} -> datatable_col={} -> visible_indices={:?} -> crosshair_pos={:?}",
1796 current_display_position, datatable_column, visible_indices, position);
1797
1798 position
1799 }
1800
1801 pub fn calculate_efficiency_metrics(&mut self, available_width: u16) -> ViewportEfficiency {
1803 let visible_indices = self.calculate_visible_column_indices(available_width);
1805
1806 let mut used_width = 0u16;
1808 let separator_width = 1u16;
1809
1810 for &col_idx in &visible_indices {
1811 let width = self.width_calculator.get_column_width(
1812 &self.dataview,
1813 &self.viewport_rows,
1814 col_idx,
1815 );
1816 used_width += width + separator_width;
1817 }
1818
1819 if !visible_indices.is_empty() {
1821 used_width = used_width.saturating_sub(separator_width);
1822 }
1823
1824 let wasted_space = available_width.saturating_sub(used_width);
1825
1826 let next_column_width = if visible_indices.is_empty() {
1828 None
1829 } else {
1830 let last_visible = *visible_indices.last().unwrap();
1831 if last_visible + 1 < self.dataview.column_count() {
1832 Some(self.width_calculator.get_column_width(
1833 &self.dataview,
1834 &self.viewport_rows,
1835 last_visible + 1,
1836 ))
1837 } else {
1838 None
1839 }
1840 };
1841
1842 let mut columns_that_could_fit = Vec::new();
1844 if wasted_space > MIN_COL_WIDTH + separator_width {
1845 let all_widths = self
1846 .width_calculator
1847 .get_all_column_widths(&self.dataview, &self.viewport_rows);
1848 for (idx, &width) in all_widths.iter().enumerate() {
1849 if !visible_indices.contains(&idx) && width + separator_width <= wasted_space {
1851 columns_that_could_fit.push((idx, width));
1852 }
1853 }
1854 }
1855
1856 let efficiency_percent = if available_width > 0 {
1857 ((f32::from(used_width) / f32::from(available_width)) * 100.0) as u8
1858 } else {
1859 0
1860 };
1861
1862 ViewportEfficiency {
1863 available_width,
1864 used_width,
1865 wasted_space,
1866 efficiency_percent,
1867 visible_columns: visible_indices.len(),
1868 column_widths: visible_indices
1869 .iter()
1870 .map(|&idx| {
1871 self.width_calculator
1872 .get_column_width(&self.dataview, &self.viewport_rows, idx)
1873 })
1874 .collect(),
1875 next_column_width,
1876 columns_that_could_fit,
1877 }
1878 }
1879
1880 pub fn navigate_to_first_column(&mut self) -> NavigationResult {
1883 if self.viewport_lock {
1885 self.crosshair_col = self.viewport_cols.start;
1887 return NavigationResult {
1888 column_position: self.crosshair_col,
1889 scroll_offset: self.viewport_cols.start,
1890 description: "Moved to first visible column (viewport locked)".to_string(),
1891 viewport_changed: false,
1892 };
1893 }
1894 let pinned_count = self.dataview.get_pinned_columns().len();
1896 let pinned_names = self.dataview.get_pinned_column_names();
1897
1898 let first_scrollable_column = pinned_count;
1900
1901 let new_scroll_offset = 0;
1903 let old_scroll_offset = self.viewport_cols.start;
1904
1905 let visible_indices = self
1907 .calculate_visible_column_indices_with_offset(self.terminal_width, new_scroll_offset);
1908 let viewport_end = if let Some(&last_idx) = visible_indices.last() {
1909 last_idx + 1
1910 } else {
1911 new_scroll_offset + 1
1912 };
1913
1914 self.viewport_cols = new_scroll_offset..viewport_end;
1916
1917 self.crosshair_col = first_scrollable_column;
1919
1920 let description = if pinned_count > 0 {
1922 format!(
1923 "First scrollable column selected (after {pinned_count} pinned: {pinned_names:?})"
1924 )
1925 } else {
1926 "First column selected".to_string()
1927 };
1928
1929 let viewport_changed = old_scroll_offset != new_scroll_offset;
1930
1931 debug!(target: "viewport_manager",
1932 "navigate_to_first_column: pinned={}, first_scrollable={}, crosshair_col={}, scroll_offset={}->{}",
1933 pinned_count, first_scrollable_column, self.crosshair_col, old_scroll_offset, new_scroll_offset);
1934
1935 NavigationResult {
1936 column_position: first_scrollable_column,
1937 scroll_offset: new_scroll_offset,
1938 description,
1939 viewport_changed,
1940 }
1941 }
1942
1943 pub fn navigate_to_last_column(&mut self) -> NavigationResult {
1946 if self.viewport_lock {
1948 self.crosshair_col = self.viewport_cols.end.saturating_sub(1);
1950 return NavigationResult {
1951 column_position: self.crosshair_col,
1952 scroll_offset: self.viewport_cols.start,
1953 description: "Moved to last visible column (viewport locked)".to_string(),
1954 viewport_changed: false,
1955 };
1956 }
1957 let display_columns = self.dataview.get_display_columns();
1959 let total_visual_columns = display_columns.len();
1960
1961 if total_visual_columns == 0 {
1962 return NavigationResult {
1963 column_position: 0,
1964 scroll_offset: 0,
1965 description: "No columns available".to_string(),
1966 viewport_changed: false,
1967 };
1968 }
1969
1970 let last_visual_column = total_visual_columns - 1;
1972
1973 self.crosshair_col = last_visual_column;
1975
1976 let available_width = self.terminal_width;
1979 let pinned_count = self.dataview.get_pinned_columns().len();
1980
1981 let mut pinned_width = 0u16;
1983 for i in 0..pinned_count {
1984 let col_idx = display_columns[i];
1985 let width = self.width_calculator.get_column_width(
1986 &self.dataview,
1987 &self.viewport_rows,
1988 col_idx,
1989 );
1990 pinned_width += width + 3; }
1992
1993 let available_for_scrollable = available_width.saturating_sub(pinned_width);
1994
1995 let mut accumulated_width = 0u16;
1997 let mut new_scroll_offset = last_visual_column;
1998
1999 for visual_idx in (pinned_count..=last_visual_column).rev() {
2001 let col_idx = display_columns[visual_idx];
2002 let width = self.width_calculator.get_column_width(
2003 &self.dataview,
2004 &self.viewport_rows,
2005 col_idx,
2006 );
2007 accumulated_width += width + 3; if accumulated_width > available_for_scrollable {
2010 new_scroll_offset = visual_idx + 1;
2012 break;
2013 }
2014 new_scroll_offset = visual_idx;
2015 }
2016
2017 new_scroll_offset = new_scroll_offset.max(pinned_count);
2019
2020 let old_scroll_offset = self.viewport_cols.start;
2021 let viewport_changed = old_scroll_offset != new_scroll_offset;
2022
2023 let visible_indices = self
2025 .calculate_visible_column_indices_with_offset(self.terminal_width, new_scroll_offset);
2026 let viewport_end = if let Some(&last_idx) = visible_indices.last() {
2027 last_idx + 1
2028 } else {
2029 new_scroll_offset + 1
2030 };
2031
2032 self.viewport_cols = new_scroll_offset..viewport_end;
2034
2035 debug!(target: "viewport_manager",
2036 "navigate_to_last_column: last_visual={}, scroll_offset={}->{}",
2037 last_visual_column, old_scroll_offset, new_scroll_offset);
2038
2039 NavigationResult {
2040 column_position: last_visual_column,
2041 scroll_offset: new_scroll_offset,
2042 description: format!("Last column selected (column {})", last_visual_column + 1),
2043 viewport_changed,
2044 }
2045 }
2046
2047 pub fn navigate_column_left(&mut self, current_display_position: usize) -> NavigationResult {
2051 if self.viewport_lock {
2053 debug!(target: "viewport_manager",
2054 "navigate_column_left: Viewport locked, crosshair_col={}, viewport={:?}",
2055 self.crosshair_col, self.viewport_cols);
2056
2057 if self.crosshair_col > self.viewport_cols.start {
2059 self.crosshair_col -= 1;
2060 return NavigationResult {
2061 column_position: self.crosshair_col,
2062 scroll_offset: self.viewport_cols.start,
2063 description: "Moved within locked viewport".to_string(),
2064 viewport_changed: false,
2065 };
2066 }
2067 return NavigationResult {
2069 column_position: self.crosshair_col,
2070 scroll_offset: self.viewport_cols.start,
2071 description: "At left edge of locked viewport".to_string(),
2072 viewport_changed: false,
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_or("unknown", std::string::String::as_str);
2140 let description = format!(
2141 "Navigate left to column '{}' ({})",
2142 column_name,
2143 new_display_index + 1
2144 );
2145
2146 debug!(target: "viewport_manager",
2147 "navigate_column_left: display_pos {}→{}, datatable_col: {}, scroll: {}→{}, viewport_changed={}",
2148 current_display_index, new_display_index, new_visual_column,
2149 old_scroll_offset, self.viewport_cols.start, viewport_changed);
2150
2151 NavigationResult {
2152 column_position: new_display_index, scroll_offset: self.viewport_cols.start,
2154 description,
2155 viewport_changed,
2156 }
2157 }
2158
2159 pub fn navigate_column_right(&mut self, current_display_position: usize) -> NavigationResult {
2162 debug!(target: "viewport_manager",
2163 "=== CRITICAL DEBUG: navigate_column_right CALLED ===");
2164 debug!(target: "viewport_manager",
2165 "Input current_display_position: {}", current_display_position);
2166 debug!(target: "viewport_manager",
2167 "Current crosshair_col: {}", self.crosshair_col);
2168 debug!(target: "viewport_manager",
2169 "Current viewport_cols: {:?}", self.viewport_cols);
2170 if self.viewport_lock {
2172 debug!(target: "viewport_manager",
2173 "navigate_column_right: Viewport locked, crosshair_col={}, viewport={:?}",
2174 self.crosshair_col, self.viewport_cols);
2175
2176 if self.crosshair_col < self.viewport_cols.end - 1 {
2178 self.crosshair_col += 1;
2179 return NavigationResult {
2180 column_position: self.crosshair_col,
2181 scroll_offset: self.viewport_cols.start,
2182 description: "Moved within locked viewport".to_string(),
2183 viewport_changed: false,
2184 };
2185 }
2186 return NavigationResult {
2188 column_position: self.crosshair_col,
2189 scroll_offset: self.viewport_cols.start,
2190 description: "At right edge of locked viewport".to_string(),
2191 viewport_changed: false,
2192 };
2193 }
2194
2195 let display_columns = self.dataview.get_display_columns();
2196 let total_display_columns = display_columns.len();
2197 let column_names = self.dataview.column_names();
2198
2199 debug!(target: "viewport_manager",
2201 "=== navigate_column_right DETAILED DEBUG ===");
2202 debug!(target: "viewport_manager",
2203 "ENTRY: current_display_pos={}, total_display_columns={}",
2204 current_display_position, total_display_columns);
2205 debug!(target: "viewport_manager",
2206 "display_columns (DataTable indices): {:?}", display_columns);
2207
2208 if current_display_position < display_columns.len() {
2210 let current_dt_idx = display_columns[current_display_position];
2211 let current_name = column_names
2212 .get(current_dt_idx)
2213 .map_or("unknown", std::string::String::as_str);
2214 debug!(target: "viewport_manager",
2215 "Current position {} -> column '{}' (dt_idx={})",
2216 current_display_position, current_name, current_dt_idx);
2217 }
2218
2219 if current_display_position + 1 < display_columns.len() {
2220 let next_dt_idx = display_columns[current_display_position + 1];
2221 let next_name = column_names
2222 .get(next_dt_idx)
2223 .map_or("unknown", std::string::String::as_str);
2224 debug!(target: "viewport_manager",
2225 "Next position {} -> column '{}' (dt_idx={})",
2226 current_display_position + 1, next_name, next_dt_idx);
2227 }
2228
2229 let current_display_index = if current_display_position < total_display_columns {
2231 current_display_position
2232 } else {
2233 debug!(target: "viewport_manager",
2234 "WARNING: current_display_position {} >= total_display_columns {}, resetting to 0",
2235 current_display_position, total_display_columns);
2236 0 };
2238
2239 debug!(target: "viewport_manager",
2240 "Validated: current_display_index={}",
2241 current_display_index);
2242
2243 if current_display_index + 1 >= total_display_columns {
2246 let last_display_index = total_display_columns.saturating_sub(1);
2248 debug!(target: "viewport_manager",
2249 "At last column boundary: current={}, total={}, returning last_display_index={}",
2250 current_display_index, total_display_columns, last_display_index);
2251 return NavigationResult {
2252 column_position: last_display_index, scroll_offset: self.viewport_cols.start,
2254 description: "Already at last column".to_string(),
2255 viewport_changed: false,
2256 };
2257 }
2258
2259 let new_display_index = current_display_index + 1;
2260
2261 let new_visual_column = display_columns
2263 .get(new_display_index)
2264 .copied()
2265 .unwrap_or_else(|| {
2266 tracing::error!(
2268 "[NAV_ERROR] Failed to get display column at index {}, total={}",
2269 new_display_index,
2270 display_columns.len()
2271 );
2272 display_columns
2274 .get(current_display_index)
2275 .copied()
2276 .unwrap_or(0)
2277 });
2278
2279 debug!(target: "viewport_manager",
2280 "navigate_column_right: display_pos {}→{}, new_visual_column={}",
2281 current_display_index, new_display_index, new_visual_column);
2282
2283 let old_scroll_offset = self.viewport_cols.start;
2284
2285 debug!(target: "viewport_manager",
2291 "navigate_column_right: moving to datatable_column={}, current viewport={:?}",
2292 new_visual_column, self.viewport_cols);
2293
2294 debug!(target: "viewport_manager",
2297 "navigate_column_right: before set_current_column(visual_idx={}), viewport={:?}",
2298 new_display_index, self.viewport_cols);
2299 let viewport_changed = self.set_current_column(new_display_index);
2300 debug!(target: "viewport_manager",
2301 "navigate_column_right: after set_current_column(visual_idx={}), viewport={:?}, changed={}",
2302 new_display_index, self.viewport_cols, viewport_changed);
2303
2304 let column_names = self.dataview.column_names();
2307 let column_name = display_columns
2308 .get(new_display_index)
2309 .and_then(|&dt_idx| column_names.get(dt_idx))
2310 .map_or("unknown", std::string::String::as_str);
2311 let description = format!(
2312 "Navigate right to column '{}' ({})",
2313 column_name,
2314 new_display_index + 1
2315 );
2316
2317 debug!(target: "viewport_manager",
2319 "=== navigate_column_right RESULT ===");
2320 debug!(target: "viewport_manager",
2321 "Returning: column_position={} (visual/display index)", new_display_index);
2322 debug!(target: "viewport_manager",
2323 "Movement: {} -> {} (visual indices)", current_display_index, new_display_index);
2324 debug!(target: "viewport_manager",
2325 "Viewport: {:?}, changed={}", self.viewport_cols, viewport_changed);
2326 debug!(target: "viewport_manager",
2327 "Description: {}", description);
2328
2329 tracing::debug!("[NAV_DEBUG] Final result: column_position={} (visual/display idx), viewport_changed={}",
2330 new_display_index, viewport_changed);
2331 debug!(target: "viewport_manager",
2332 "navigate_column_right EXIT: display_pos {}→{}, datatable_col: {}, viewport: {:?}, scroll: {}→{}, viewport_changed={}",
2333 current_display_index, new_display_index, new_visual_column,
2334 self.viewport_cols, old_scroll_offset, self.viewport_cols.start, viewport_changed);
2335
2336 NavigationResult {
2337 column_position: new_display_index, scroll_offset: self.viewport_cols.start,
2339 description,
2340 viewport_changed,
2341 }
2342 }
2343
2344 pub fn page_down(&mut self) -> RowNavigationResult {
2346 let total_rows = self.dataview.row_count();
2347 let visible_rows = self.terminal_height.saturating_sub(6) as usize; debug!(target: "viewport_manager",
2351 "page_down: crosshair_row={}, total_rows={}, visible_rows={}, current_viewport_rows={:?}",
2352 self.crosshair_row, total_rows, visible_rows, self.viewport_rows);
2353
2354 if self.viewport_lock {
2356 debug!(target: "viewport_manager",
2357 "page_down: Viewport locked, moving within current viewport");
2358 let new_row = self
2360 .viewport_rows
2361 .end
2362 .saturating_sub(1)
2363 .min(total_rows.saturating_sub(1));
2364 self.crosshair_row = new_row;
2365 return RowNavigationResult {
2366 row_position: new_row,
2367 row_scroll_offset: self.viewport_rows.start,
2368 description: format!(
2369 "Page down within locked viewport: row {} → {}",
2370 self.crosshair_row + 1,
2371 new_row + 1
2372 ),
2373 viewport_changed: false,
2374 };
2375 }
2376
2377 if self.cursor_lock {
2379 if let Some(lock_position) = self.cursor_lock_position {
2380 debug!(target: "viewport_manager",
2381 "page_down: Cursor locked at position {}", lock_position);
2382
2383 let old_scroll_offset = self.viewport_rows.start;
2385 let max_scroll = total_rows.saturating_sub(visible_rows);
2386 let new_scroll_offset = (old_scroll_offset + visible_rows).min(max_scroll);
2387
2388 if new_scroll_offset == old_scroll_offset {
2389 return RowNavigationResult {
2391 row_position: self.crosshair_row,
2392 row_scroll_offset: old_scroll_offset,
2393 description: "Already at bottom".to_string(),
2394 viewport_changed: false,
2395 };
2396 }
2397
2398 self.viewport_rows =
2400 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2401
2402 self.crosshair_row =
2404 (new_scroll_offset + lock_position).min(total_rows.saturating_sub(1));
2405
2406 return RowNavigationResult {
2407 row_position: self.crosshair_row,
2408 row_scroll_offset: new_scroll_offset,
2409 description: format!(
2410 "Page down with cursor lock (viewport {} → {})",
2411 old_scroll_offset + 1,
2412 new_scroll_offset + 1
2413 ),
2414 viewport_changed: true,
2415 };
2416 }
2417 }
2418
2419 let new_row = (self.crosshair_row + visible_rows).min(total_rows.saturating_sub(1));
2422 self.crosshair_row = new_row;
2423
2424 let old_scroll_offset = self.viewport_rows.start;
2426 let new_scroll_offset = if new_row >= self.viewport_rows.start + visible_rows {
2427 (new_row + 1).saturating_sub(visible_rows)
2429 } else {
2430 old_scroll_offset
2432 };
2433
2434 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2436 let viewport_changed = new_scroll_offset != old_scroll_offset;
2437
2438 let description = format!(
2439 "Page down: row {} → {} (of {})",
2440 self.crosshair_row + 1,
2441 new_row + 1,
2442 total_rows
2443 );
2444
2445 debug!(target: "viewport_manager",
2446 "page_down result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2447 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2448
2449 RowNavigationResult {
2450 row_position: new_row,
2451 row_scroll_offset: new_scroll_offset,
2452 description,
2453 viewport_changed,
2454 }
2455 }
2456
2457 pub fn page_up(&mut self) -> RowNavigationResult {
2459 let total_rows = self.dataview.row_count();
2460 let visible_rows = self.terminal_height.saturating_sub(6) as usize; debug!(target: "viewport_manager",
2464 "page_up: crosshair_row={}, visible_rows={}, current_viewport_rows={:?}",
2465 self.crosshair_row, visible_rows, self.viewport_rows);
2466
2467 if self.viewport_lock {
2469 debug!(target: "viewport_manager",
2470 "page_up: Viewport locked, moving within current viewport");
2471 let new_row = self.viewport_rows.start;
2473 self.crosshair_row = new_row;
2474 return RowNavigationResult {
2475 row_position: new_row,
2476 row_scroll_offset: self.viewport_rows.start,
2477 description: format!(
2478 "Page up within locked viewport: row {} → {}",
2479 self.crosshair_row + 1,
2480 new_row + 1
2481 ),
2482 viewport_changed: false,
2483 };
2484 }
2485
2486 if self.cursor_lock {
2488 if let Some(lock_position) = self.cursor_lock_position {
2489 debug!(target: "viewport_manager",
2490 "page_up: Cursor locked at position {}", lock_position);
2491
2492 let old_scroll_offset = self.viewport_rows.start;
2494 let new_scroll_offset = old_scroll_offset.saturating_sub(visible_rows);
2495
2496 if new_scroll_offset == old_scroll_offset {
2497 return RowNavigationResult {
2499 row_position: self.crosshair_row,
2500 row_scroll_offset: old_scroll_offset,
2501 description: "Already at top".to_string(),
2502 viewport_changed: false,
2503 };
2504 }
2505
2506 self.viewport_rows =
2508 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2509
2510 self.crosshair_row = new_scroll_offset + lock_position;
2512
2513 return RowNavigationResult {
2514 row_position: self.crosshair_row,
2515 row_scroll_offset: new_scroll_offset,
2516 description: format!(
2517 "Page up with cursor lock (viewport {} → {})",
2518 old_scroll_offset + 1,
2519 new_scroll_offset + 1
2520 ),
2521 viewport_changed: true,
2522 };
2523 }
2524 }
2525
2526 let new_row = self.crosshair_row.saturating_sub(visible_rows);
2529 self.crosshair_row = new_row;
2530
2531 let old_scroll_offset = self.viewport_rows.start;
2533 let new_scroll_offset = if new_row < self.viewport_rows.start {
2534 new_row
2536 } else {
2537 old_scroll_offset
2539 };
2540
2541 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2543 let viewport_changed = new_scroll_offset != old_scroll_offset;
2544
2545 let description = format!("Page up: row {} → {}", self.crosshair_row + 1, new_row + 1);
2546
2547 debug!(target: "viewport_manager",
2548 "page_up result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2549 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2550
2551 RowNavigationResult {
2552 row_position: new_row,
2553 row_scroll_offset: new_scroll_offset,
2554 description,
2555 viewport_changed,
2556 }
2557 }
2558
2559 pub fn half_page_down(&mut self) -> RowNavigationResult {
2561 let total_rows = self.dataview.row_count();
2562 let visible_rows = self.terminal_height.saturating_sub(6) as usize; let half_page = visible_rows / 2;
2565
2566 debug!(target: "viewport_manager",
2567 "half_page_down: crosshair_row={}, total_rows={}, half_page={}, current_viewport_rows={:?}",
2568 self.crosshair_row, total_rows, half_page, self.viewport_rows);
2569
2570 if self.viewport_lock {
2572 debug!(target: "viewport_manager",
2573 "half_page_down: Viewport locked, moving within current viewport");
2574 let new_row = self
2576 .viewport_rows
2577 .end
2578 .saturating_sub(1)
2579 .min(total_rows.saturating_sub(1));
2580 self.crosshair_row = new_row;
2581 return RowNavigationResult {
2582 row_position: new_row,
2583 row_scroll_offset: self.viewport_rows.start,
2584 description: format!(
2585 "Half page down within locked viewport: row {} → {}",
2586 self.crosshair_row + 1,
2587 new_row + 1
2588 ),
2589 viewport_changed: false,
2590 };
2591 }
2592
2593 if self.cursor_lock {
2595 if let Some(lock_position) = self.cursor_lock_position {
2596 debug!(target: "viewport_manager",
2597 "half_page_down: Cursor locked at position {}", lock_position);
2598
2599 let old_scroll_offset = self.viewport_rows.start;
2601 let max_scroll = total_rows.saturating_sub(visible_rows);
2602 let new_scroll_offset = (old_scroll_offset + half_page).min(max_scroll);
2603
2604 if new_scroll_offset == old_scroll_offset {
2605 return RowNavigationResult {
2607 row_position: self.crosshair_row,
2608 row_scroll_offset: old_scroll_offset,
2609 description: "Already at bottom".to_string(),
2610 viewport_changed: false,
2611 };
2612 }
2613
2614 self.viewport_rows =
2616 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2617
2618 self.crosshair_row =
2620 (new_scroll_offset + lock_position).min(total_rows.saturating_sub(1));
2621
2622 return RowNavigationResult {
2623 row_position: self.crosshair_row,
2624 row_scroll_offset: new_scroll_offset,
2625 description: format!(
2626 "Half page down with cursor lock (viewport {} → {})",
2627 old_scroll_offset + 1,
2628 new_scroll_offset + 1
2629 ),
2630 viewport_changed: true,
2631 };
2632 }
2633 }
2634
2635 let new_row = (self.crosshair_row + half_page).min(total_rows.saturating_sub(1));
2638 self.crosshair_row = new_row;
2639
2640 let old_scroll_offset = self.viewport_rows.start;
2642 let new_scroll_offset = if new_row >= self.viewport_rows.start + visible_rows {
2643 (new_row + 1).saturating_sub(visible_rows)
2645 } else {
2646 old_scroll_offset
2648 };
2649
2650 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2652 let viewport_changed = new_scroll_offset != old_scroll_offset;
2653
2654 let description = format!(
2655 "Half page down: row {} → {} (of {})",
2656 self.crosshair_row + 1 - half_page.min(self.crosshair_row),
2657 new_row + 1,
2658 total_rows
2659 );
2660
2661 debug!(target: "viewport_manager",
2662 "half_page_down result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2663 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2664
2665 RowNavigationResult {
2666 row_position: new_row,
2667 row_scroll_offset: new_scroll_offset,
2668 description,
2669 viewport_changed,
2670 }
2671 }
2672
2673 pub fn half_page_up(&mut self) -> RowNavigationResult {
2675 let total_rows = self.dataview.row_count();
2676 let visible_rows = self.terminal_height.saturating_sub(6) as usize; let half_page = visible_rows / 2;
2679
2680 debug!(target: "viewport_manager",
2681 "half_page_up: crosshair_row={}, half_page={}, current_viewport_rows={:?}",
2682 self.crosshair_row, half_page, self.viewport_rows);
2683
2684 if self.viewport_lock {
2686 debug!(target: "viewport_manager",
2687 "half_page_up: Viewport locked, moving within current viewport");
2688 let new_row = self.viewport_rows.start;
2690 self.crosshair_row = new_row;
2691 return RowNavigationResult {
2692 row_position: new_row,
2693 row_scroll_offset: self.viewport_rows.start,
2694 description: format!(
2695 "Half page up within locked viewport: row {} → {}",
2696 self.crosshair_row + 1,
2697 new_row + 1
2698 ),
2699 viewport_changed: false,
2700 };
2701 }
2702
2703 if self.cursor_lock {
2705 if let Some(lock_position) = self.cursor_lock_position {
2706 debug!(target: "viewport_manager",
2707 "half_page_up: Cursor locked at position {}", lock_position);
2708
2709 let old_scroll_offset = self.viewport_rows.start;
2711 let new_scroll_offset = old_scroll_offset.saturating_sub(half_page);
2712
2713 if new_scroll_offset == old_scroll_offset {
2714 return RowNavigationResult {
2716 row_position: self.crosshair_row,
2717 row_scroll_offset: old_scroll_offset,
2718 description: "Already at top".to_string(),
2719 viewport_changed: false,
2720 };
2721 }
2722
2723 self.viewport_rows =
2725 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2726
2727 self.crosshair_row = new_scroll_offset + lock_position;
2729
2730 return RowNavigationResult {
2731 row_position: self.crosshair_row,
2732 row_scroll_offset: new_scroll_offset,
2733 description: format!(
2734 "Half page up with cursor lock (viewport {} → {})",
2735 old_scroll_offset + 1,
2736 new_scroll_offset + 1
2737 ),
2738 viewport_changed: true,
2739 };
2740 }
2741 }
2742
2743 let new_row = self.crosshair_row.saturating_sub(half_page);
2746 self.crosshair_row = new_row;
2747
2748 let old_scroll_offset = self.viewport_rows.start;
2750 let new_scroll_offset = if new_row < self.viewport_rows.start {
2751 new_row
2753 } else {
2754 old_scroll_offset
2756 };
2757
2758 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2760 let viewport_changed = new_scroll_offset != old_scroll_offset;
2761
2762 let description = format!(
2763 "Half page up: row {} → {}",
2764 self.crosshair_row + half_page + 1,
2765 new_row + 1
2766 );
2767
2768 debug!(target: "viewport_manager",
2769 "half_page_up result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2770 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2771
2772 RowNavigationResult {
2773 row_position: new_row,
2774 row_scroll_offset: new_scroll_offset,
2775 description,
2776 viewport_changed,
2777 }
2778 }
2779
2780 pub fn navigate_to_last_row(&mut self, total_rows: usize) -> RowNavigationResult {
2782 if self.viewport_lock {
2784 let last_visible = self
2786 .viewport_rows
2787 .end
2788 .saturating_sub(1)
2789 .min(total_rows.saturating_sub(1));
2790 self.crosshair_row = last_visible;
2791 return RowNavigationResult {
2792 row_position: self.crosshair_row,
2793 row_scroll_offset: self.viewport_rows.start,
2794 description: "Moved to last visible row (viewport locked)".to_string(),
2795 viewport_changed: false,
2796 };
2797 }
2798 if total_rows == 0 {
2799 return RowNavigationResult {
2800 row_position: 0,
2801 row_scroll_offset: 0,
2802 description: "No rows to navigate".to_string(),
2803 viewport_changed: false,
2804 };
2805 }
2806
2807 let visible_rows = (self.terminal_height as usize).max(10);
2810
2811 let last_row = total_rows - 1;
2813
2814 let new_scroll_offset = total_rows.saturating_sub(visible_rows);
2818
2819 debug!(target: "viewport_manager",
2820 "navigate_to_last_row: total_rows={}, last_row={}, visible_rows={}, new_scroll_offset={}",
2821 total_rows, last_row, visible_rows, new_scroll_offset);
2822
2823 let old_scroll_offset = self.viewport_rows.start;
2825 let viewport_changed = new_scroll_offset != old_scroll_offset;
2826
2827 self.viewport_rows = new_scroll_offset..total_rows.min(new_scroll_offset + visible_rows);
2829
2830 self.crosshair_row = last_row;
2833
2834 let description = format!("Jumped to last row ({}/{})", last_row + 1, total_rows);
2835
2836 debug!(target: "viewport_manager",
2837 "navigate_to_last_row result: row={}, crosshair_row={}, scroll_offset={}→{}, viewport_changed={}",
2838 last_row, self.crosshair_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2839
2840 RowNavigationResult {
2841 row_position: last_row,
2842 row_scroll_offset: new_scroll_offset,
2843 description,
2844 viewport_changed,
2845 }
2846 }
2847
2848 pub fn navigate_to_first_row(&mut self, total_rows: usize) -> RowNavigationResult {
2850 if self.viewport_lock {
2852 self.crosshair_row = self.viewport_rows.start;
2854 return RowNavigationResult {
2855 row_position: self.crosshair_row,
2856 row_scroll_offset: self.viewport_rows.start,
2857 description: "Moved to first visible row (viewport locked)".to_string(),
2858 viewport_changed: false,
2859 };
2860 }
2861 if total_rows == 0 {
2862 return RowNavigationResult {
2863 row_position: 0,
2864 row_scroll_offset: 0,
2865 description: "No rows to navigate".to_string(),
2866 viewport_changed: false,
2867 };
2868 }
2869
2870 let visible_rows = (self.terminal_height as usize).max(10);
2873
2874 let first_row = 0;
2876
2877 let new_scroll_offset = 0;
2879
2880 debug!(target: "viewport_manager",
2881 "navigate_to_first_row: total_rows={}, visible_rows={}",
2882 total_rows, visible_rows);
2883
2884 let old_scroll_offset = self.viewport_rows.start;
2886 let viewport_changed = new_scroll_offset != old_scroll_offset;
2887
2888 self.viewport_rows = 0..visible_rows.min(total_rows);
2890
2891 self.crosshair_row = first_row;
2893
2894 let description = format!("Jumped to first row (1/{total_rows})");
2895
2896 debug!(target: "viewport_manager",
2897 "navigate_to_first_row result: row=0, crosshair_row={}, scroll_offset={}→0, viewport_changed={}",
2898 self.crosshair_row, old_scroll_offset, viewport_changed);
2899
2900 RowNavigationResult {
2901 row_position: first_row,
2902 row_scroll_offset: new_scroll_offset,
2903 description,
2904 viewport_changed,
2905 }
2906 }
2907
2908 pub fn navigate_to_viewport_top(&mut self) -> RowNavigationResult {
2910 let top_row = self.viewport_rows.start;
2911 let old_row = self.crosshair_row;
2912
2913 self.crosshair_row = top_row;
2915
2916 let description = format!("Moved to viewport top (row {})", top_row + 1);
2917
2918 debug!(target: "viewport_manager",
2919 "navigate_to_viewport_top: crosshair {} -> {}",
2920 old_row, self.crosshair_row);
2921
2922 RowNavigationResult {
2923 row_position: self.crosshair_row,
2924 row_scroll_offset: self.viewport_rows.start,
2925 description,
2926 viewport_changed: false, }
2928 }
2929
2930 pub fn navigate_to_viewport_middle(&mut self) -> RowNavigationResult {
2932 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
2934 let middle_offset = viewport_height / 2;
2935 let middle_row = self.viewport_rows.start + middle_offset;
2936 let old_row = self.crosshair_row;
2937
2938 self.crosshair_row = middle_row;
2940
2941 let description = format!("Moved to viewport middle (row {})", middle_row + 1);
2942
2943 debug!(target: "viewport_manager",
2944 "navigate_to_viewport_middle: crosshair {} -> {}",
2945 old_row, self.crosshair_row);
2946
2947 RowNavigationResult {
2948 row_position: self.crosshair_row,
2949 row_scroll_offset: self.viewport_rows.start,
2950 description,
2951 viewport_changed: false, }
2953 }
2954
2955 pub fn navigate_to_viewport_bottom(&mut self) -> RowNavigationResult {
2957 let bottom_row = self.viewport_rows.end.saturating_sub(1);
2960 let old_row = self.crosshair_row;
2961
2962 self.crosshair_row = bottom_row;
2964
2965 let description = format!("Moved to viewport bottom (row {})", bottom_row + 1);
2966
2967 debug!(target: "viewport_manager",
2968 "navigate_to_viewport_bottom: crosshair {} -> {}",
2969 old_row, self.crosshair_row);
2970
2971 RowNavigationResult {
2972 row_position: self.crosshair_row,
2973 row_scroll_offset: self.viewport_rows.start,
2974 description,
2975 viewport_changed: false, }
2977 }
2978
2979 pub fn toggle_cursor_lock(&mut self) -> (bool, String) {
2982 self.cursor_lock = !self.cursor_lock;
2983
2984 if self.cursor_lock {
2985 let relative_position = self.crosshair_row.saturating_sub(self.viewport_rows.start);
2987 self.cursor_lock_position = Some(relative_position);
2988
2989 let description = format!(
2990 "Cursor lock: ON (locked at viewport position {})",
2991 relative_position + 1
2992 );
2993 debug!(target: "viewport_manager",
2994 "Cursor lock enabled: crosshair at viewport position {}",
2995 relative_position);
2996 (true, description)
2997 } else {
2998 self.cursor_lock_position = None;
2999 let description = "Cursor lock: OFF".to_string();
3000 debug!(target: "viewport_manager", "Cursor lock disabled");
3001 (false, description)
3002 }
3003 }
3004
3005 pub fn toggle_viewport_lock(&mut self) -> (bool, String) {
3007 self.viewport_lock = !self.viewport_lock;
3008
3009 if self.viewport_lock {
3010 self.viewport_lock_boundaries = Some(self.viewport_rows.clone());
3012
3013 let description = format!(
3014 "Viewport lock: ON (no scrolling, cursor constrained to rows {}-{})",
3015 self.viewport_rows.start + 1,
3016 self.viewport_rows.end
3017 );
3018 debug!(target: "viewport_manager",
3019 "VIEWPORT LOCK ENABLED: boundaries {:?}, crosshair={}, viewport={:?}",
3020 self.viewport_lock_boundaries, self.crosshair_row, self.viewport_rows);
3021 (true, description)
3022 } else {
3023 self.viewport_lock_boundaries = None;
3024 let description = "Viewport lock: OFF (normal scrolling)".to_string();
3025 debug!(target: "viewport_manager", "VIEWPORT LOCK DISABLED");
3026 (false, description)
3027 }
3028 }
3029
3030 #[must_use]
3032 pub fn is_cursor_locked(&self) -> bool {
3033 self.cursor_lock
3034 }
3035
3036 #[must_use]
3038 pub fn is_viewport_locked(&self) -> bool {
3039 self.viewport_lock
3040 }
3041
3042 pub fn lock_viewport(&mut self) {
3044 if !self.viewport_lock {
3045 self.viewport_lock = true;
3046 self.viewport_lock_boundaries = Some(self.viewport_rows.clone());
3047 debug!(target: "viewport_manager", "Viewport locked: rows {}-{}",
3048 self.viewport_rows.start + 1, self.viewport_rows.end);
3049 }
3050 }
3051
3052 pub fn unlock_viewport(&mut self) {
3054 if self.viewport_lock {
3055 self.viewport_lock = false;
3056 self.viewport_lock_boundaries = None;
3057 debug!(target: "viewport_manager", "Viewport unlocked");
3058 }
3059 }
3060
3061 pub fn reorder_column_left(&mut self, current_column: usize) -> ColumnReorderResult {
3063 debug!(target: "viewport_manager",
3064 "reorder_column_left: current_column={}, viewport={:?}",
3065 current_column, self.viewport_cols
3066 );
3067
3068 let column_count = self.dataview.column_count();
3070
3071 if current_column >= column_count {
3072 return ColumnReorderResult {
3073 new_column_position: current_column,
3074 description: "Invalid column position".to_string(),
3075 success: false,
3076 };
3077 }
3078
3079 let pinned_count = self.dataview.get_pinned_columns().len();
3081
3082 debug!(target: "viewport_manager",
3083 "Before move: column_count={}, pinned_count={}, current_column={}",
3084 column_count, pinned_count, current_column
3085 );
3086
3087 let mut new_dataview = (*self.dataview).clone();
3089
3090 let success = new_dataview.move_column_left(current_column);
3092
3093 if success {
3094 self.dataview = Arc::new(new_dataview);
3096 }
3097
3098 if success {
3099 self.invalidate_cache(); let wrapped_to_end =
3103 current_column == 0 || (current_column == pinned_count && pinned_count > 0);
3104 let new_position = if wrapped_to_end {
3105 column_count - 1
3107 } else {
3108 current_column - 1
3110 };
3111
3112 let column_names = self.dataview.column_names();
3113 let column_name = column_names
3114 .get(new_position)
3115 .map_or("?", std::string::String::as_str);
3116
3117 debug!(target: "viewport_manager",
3118 "After move: new_position={}, wrapped_to_end={}, column_name={}",
3119 new_position, wrapped_to_end, column_name
3120 );
3121
3122 if wrapped_to_end {
3124 let optimal_offset = self.calculate_optimal_offset_for_last_column(
3126 self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH),
3127 );
3128 debug!(target: "viewport_manager",
3129 "Column wrapped to end! Adjusting viewport from {:?} to {}..{}",
3130 self.viewport_cols, optimal_offset, self.dataview.column_count()
3131 );
3132 self.viewport_cols = optimal_offset..self.dataview.column_count();
3133 } else {
3134 if !self.viewport_cols.contains(&new_position) {
3136 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3138
3139 let columns_that_fit =
3141 self.calculate_columns_that_fit(new_position, terminal_width);
3142
3143 let new_start = if new_position < self.viewport_cols.start {
3145 new_position
3147 } else {
3148 new_position.saturating_sub(columns_that_fit - 1)
3150 };
3151
3152 let new_end = (new_start + columns_that_fit).min(self.dataview.column_count());
3153 self.viewport_cols = new_start..new_end;
3154
3155 debug!(target: "viewport_manager",
3156 "Column moved outside viewport! Adjusting viewport to {}..{} to show column {} at position {}",
3157 new_start, new_end, column_name, new_position
3158 );
3159 }
3160 }
3161
3162 self.crosshair_col = new_position;
3164
3165 ColumnReorderResult {
3166 new_column_position: new_position,
3167 description: format!("Moved column '{column_name}' left"),
3168 success: true,
3169 }
3170 } else {
3171 ColumnReorderResult {
3172 new_column_position: current_column,
3173 description: "Cannot move column left".to_string(),
3174 success: false,
3175 }
3176 }
3177 }
3178
3179 pub fn reorder_column_right(&mut self, current_column: usize) -> ColumnReorderResult {
3181 let column_count = self.dataview.column_count();
3183
3184 if current_column >= column_count {
3185 return ColumnReorderResult {
3186 new_column_position: current_column,
3187 description: "Invalid column position".to_string(),
3188 success: false,
3189 };
3190 }
3191
3192 let pinned_count = self.dataview.get_pinned_columns().len();
3194
3195 let mut new_dataview = (*self.dataview).clone();
3197
3198 let success = new_dataview.move_column_right(current_column);
3200
3201 if success {
3202 self.dataview = Arc::new(new_dataview);
3204 }
3205
3206 if success {
3207 self.invalidate_cache(); let wrapped_to_beginning = current_column == column_count - 1
3211 || (pinned_count > 0 && current_column == pinned_count - 1);
3212
3213 let new_position = if current_column == column_count - 1 {
3214 if pinned_count > 0 {
3216 pinned_count } else {
3218 0 }
3220 } else if pinned_count > 0 && current_column == pinned_count - 1 {
3221 0
3223 } else {
3224 current_column + 1
3226 };
3227
3228 let column_names = self.dataview.column_names();
3229 let column_name = column_names
3230 .get(new_position)
3231 .map_or("?", std::string::String::as_str);
3232
3233 if wrapped_to_beginning {
3235 self.viewport_cols = 0..self.dataview.column_count().min(20); debug!(target: "viewport_manager",
3238 "Column wrapped to beginning, resetting viewport to show column {} at position {}",
3239 column_name, new_position
3240 );
3241 } else {
3242 if !self.viewport_cols.contains(&new_position) {
3244 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3246
3247 let columns_that_fit =
3249 self.calculate_columns_that_fit(new_position, terminal_width);
3250
3251 let new_start = if new_position > self.viewport_cols.end {
3253 new_position.saturating_sub(columns_that_fit - 1)
3255 } else {
3256 new_position
3258 };
3259
3260 let new_end = (new_start + columns_that_fit).min(self.dataview.column_count());
3261 self.viewport_cols = new_start..new_end;
3262
3263 debug!(target: "viewport_manager",
3264 "Column moved outside viewport! Adjusting viewport to {}..{} to show column {} at position {}",
3265 new_start, new_end, column_name, new_position
3266 );
3267 }
3268 }
3269
3270 self.crosshair_col = new_position;
3272
3273 ColumnReorderResult {
3274 new_column_position: new_position,
3275 description: format!("Moved column '{column_name}' right"),
3276 success: true,
3277 }
3278 } else {
3279 ColumnReorderResult {
3280 new_column_position: current_column,
3281 description: "Cannot move column right".to_string(),
3282 success: false,
3283 }
3284 }
3285 }
3286
3287 pub fn hide_column(&mut self, column_index: usize) -> bool {
3290 debug!(target: "viewport_manager", "hide_column: column_index={}", column_index);
3291
3292 let mut new_dataview = (*self.dataview).clone();
3294
3295 let success = new_dataview.hide_column(column_index);
3297
3298 if success {
3299 self.dataview = Arc::new(new_dataview);
3301 self.invalidate_cache(); let column_count = self.dataview.column_count();
3305 if self.viewport_cols.end > column_count {
3306 self.viewport_cols.end = column_count;
3307 }
3308 if self.viewport_cols.start >= column_count && column_count > 0 {
3309 self.viewport_cols.start = column_count - 1;
3310 }
3311
3312 if column_index == self.crosshair_col {
3315 if column_count > 0 {
3317 if self.crosshair_col >= column_count {
3320 self.crosshair_col = column_count - 1;
3321 }
3322 } else {
3325 self.crosshair_col = 0;
3326 }
3327 debug!(target: "viewport_manager", "Crosshair was on hidden column, moved to {}", self.crosshair_col);
3328 } else if column_index < self.crosshair_col {
3329 self.crosshair_col = self.crosshair_col.saturating_sub(1);
3331 debug!(target: "viewport_manager", "Hidden column was before crosshair, adjusted crosshair to {}", self.crosshair_col);
3332 }
3333
3334 debug!(target: "viewport_manager", "Column {} hidden successfully", column_index);
3335 } else {
3336 debug!(target: "viewport_manager", "Failed to hide column {} (might be pinned)", column_index);
3337 }
3338
3339 success
3340 }
3341
3342 pub fn hide_column_by_name(&mut self, column_name: &str) -> bool {
3345 debug!(target: "viewport_manager", "hide_column_by_name: column_name={}", column_name);
3346
3347 let mut new_dataview = (*self.dataview).clone();
3349
3350 let success = new_dataview.hide_column_by_name(column_name);
3352
3353 if success {
3354 self.dataview = Arc::new(new_dataview);
3356 }
3357
3358 if success {
3359 self.invalidate_cache(); let column_count = self.dataview.column_count();
3363 if self.viewport_cols.end > column_count {
3364 self.viewport_cols.end = column_count;
3365 }
3366 if self.viewport_cols.start >= column_count && column_count > 0 {
3367 self.viewport_cols.start = column_count - 1;
3368 }
3369
3370 if self.crosshair_col >= column_count && column_count > 0 {
3372 self.crosshair_col = column_count - 1;
3373 debug!(target: "viewport_manager", "Adjusted crosshair to {} after hiding column", self.crosshair_col);
3374 }
3375
3376 debug!(target: "viewport_manager", "Column '{}' hidden successfully", column_name);
3377 } else {
3378 debug!(target: "viewport_manager", "Failed to hide column '{}' (might be pinned or not found)", column_name);
3379 }
3380
3381 success
3382 }
3383
3384 pub fn hide_empty_columns(&mut self) -> usize {
3387 debug!(target: "viewport_manager", "hide_empty_columns called");
3388
3389 let mut new_dataview = (*self.dataview).clone();
3391
3392 let count = new_dataview.hide_empty_columns();
3394
3395 if count > 0 {
3396 self.dataview = Arc::new(new_dataview);
3398 }
3399
3400 if count > 0 {
3401 self.invalidate_cache(); let column_count = self.dataview.column_count();
3405 if self.viewport_cols.end > column_count {
3406 self.viewport_cols.end = column_count;
3407 }
3408 if self.viewport_cols.start >= column_count && column_count > 0 {
3409 self.viewport_cols.start = column_count - 1;
3410 }
3411
3412 debug!(target: "viewport_manager", "Hidden {} empty columns", count);
3413 }
3414
3415 count
3416 }
3417
3418 pub fn unhide_all_columns(&mut self) {
3420 debug!(target: "viewport_manager", "unhide_all_columns called");
3421
3422 let mut new_dataview = (*self.dataview).clone();
3424
3425 new_dataview.unhide_all_columns();
3427
3428 self.dataview = Arc::new(new_dataview);
3430
3431 self.invalidate_cache(); let column_count = self.dataview.column_count();
3435 self.viewport_cols = 0..column_count.min(20); debug!(target: "viewport_manager", "All columns unhidden, viewport reset to {:?}", self.viewport_cols);
3438 }
3439
3440 pub fn pin_column(&mut self, column_index: usize) -> bool {
3443 debug!(target: "viewport_manager", "pin_column: column_index={}", column_index);
3444
3445 let mut new_dataview = (*self.dataview).clone();
3447
3448 let success = new_dataview.pin_column(column_index).is_ok();
3450
3451 if success {
3452 self.dataview = Arc::new(new_dataview);
3454 self.invalidate_cache(); debug!(target: "viewport_manager", "Column {} pinned successfully", column_index);
3457 } else {
3458 debug!(target: "viewport_manager", "Failed to pin column {}", column_index);
3459 }
3460
3461 success
3462 }
3463
3464 pub fn set_current_column(&mut self, visual_column: usize) -> bool {
3467 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH); let total_visual_columns = self.dataview.get_display_columns().len();
3469
3470 tracing::debug!("[PIN_DEBUG] === set_current_column ===");
3471 tracing::debug!(
3472 "[PIN_DEBUG] visual_column={}, viewport_cols={:?}",
3473 visual_column,
3474 self.viewport_cols
3475 );
3476 tracing::debug!(
3477 "[PIN_DEBUG] terminal_width={}, total_visual_columns={}",
3478 terminal_width,
3479 total_visual_columns
3480 );
3481
3482 debug!(target: "viewport_manager",
3483 "set_current_column ENTRY: visual_column={}, current_viewport={:?}, terminal_width={}, total_visual={}",
3484 visual_column, self.viewport_cols, terminal_width, total_visual_columns);
3485
3486 if visual_column >= total_visual_columns {
3488 debug!(target: "viewport_manager", "Visual column {} out of bounds (max {})", visual_column, total_visual_columns);
3489 tracing::debug!(
3490 "[PIN_DEBUG] Column {} out of bounds (max {})",
3491 visual_column,
3492 total_visual_columns
3493 );
3494 return false;
3495 }
3496
3497 self.crosshair_col = visual_column;
3499 debug!(target: "viewport_manager", "Updated crosshair_col to {}", visual_column);
3500 tracing::debug!("[PIN_DEBUG] Updated crosshair_col to {}", visual_column);
3501
3502 let display_columns = self.dataview.get_display_columns();
3505 let mut total_width_needed = 0u16;
3506 for &dt_idx in &display_columns {
3507 let width =
3508 self.width_calculator
3509 .get_column_width(&self.dataview, &self.viewport_rows, dt_idx);
3510 total_width_needed += width + 1; }
3512
3513 if total_width_needed <= terminal_width {
3514 debug!(target: "viewport_manager",
3516 "Visual column {} in optimal layout mode (all columns fit), no adjustment needed", visual_column);
3517 tracing::debug!("[PIN_DEBUG] All columns fit, no adjustment needed");
3518 tracing::debug!("[PIN_DEBUG] === End set_current_column (all fit) ===");
3519 return false;
3520 }
3521
3522 let pinned_count = self.dataview.get_pinned_columns().len();
3525 tracing::debug!("[PIN_DEBUG] pinned_count={}", pinned_count);
3526
3527 let visible_columns = self.calculate_visible_column_indices(terminal_width);
3529 let display_columns = self.dataview.get_display_columns();
3530
3531 let target_dt_idx = if visual_column < display_columns.len() {
3533 display_columns[visual_column]
3534 } else {
3535 tracing::debug!("[PIN_DEBUG] Column {} out of bounds", visual_column);
3536 return false;
3537 };
3538
3539 let is_visible = visible_columns.contains(&target_dt_idx);
3540 tracing::debug!(
3541 "[PIN_DEBUG] Column {} (dt_idx={}) visible check: visible_columns={:?}, is_visible={}",
3542 visual_column,
3543 target_dt_idx,
3544 visible_columns,
3545 is_visible
3546 );
3547
3548 debug!(target: "viewport_manager",
3549 "set_current_column CHECK: visual_column={}, viewport={:?}, is_visible={}",
3550 visual_column, self.viewport_cols, is_visible);
3551
3552 if is_visible {
3553 debug!(target: "viewport_manager", "Visual column {} already visible in viewport {:?}, no adjustment needed",
3554 visual_column, self.viewport_cols);
3555 tracing::debug!("[PIN_DEBUG] Column already visible, no adjustment");
3556 tracing::debug!("[PIN_DEBUG] === End set_current_column (no change) ===");
3557 return false;
3558 }
3559
3560 debug!(target: "viewport_manager", "Visual column {} NOT visible, calculating new offset", visual_column);
3562 let new_scroll_offset = self.calculate_scroll_offset_for_visual_column(visual_column);
3563 let old_scroll_offset = self.viewport_cols.start;
3564
3565 debug!(target: "viewport_manager", "Calculated new_scroll_offset={}, old_scroll_offset={}",
3566 new_scroll_offset, old_scroll_offset);
3567
3568 if new_scroll_offset != old_scroll_offset {
3569 let display_columns = self.dataview.get_display_columns();
3572 let pinned_count = self.dataview.get_pinned_columns().len();
3573 let mut used_width = 0u16;
3574 let separator_width = 1u16;
3575
3576 for visual_idx in 0..pinned_count {
3578 if visual_idx < display_columns.len() {
3579 let dt_idx = display_columns[visual_idx];
3580 let width = self.width_calculator.get_column_width(
3581 &self.dataview,
3582 &self.viewport_rows,
3583 dt_idx,
3584 );
3585 used_width += width + separator_width;
3586 }
3587 }
3588
3589 let mut scrollable_columns_that_fit = 0;
3591 let visual_start = pinned_count + new_scroll_offset;
3592
3593 for visual_idx in visual_start..display_columns.len() {
3594 let dt_idx = display_columns[visual_idx];
3595 let width = self.width_calculator.get_column_width(
3596 &self.dataview,
3597 &self.viewport_rows,
3598 dt_idx,
3599 );
3600 if used_width + width + separator_width <= terminal_width {
3601 used_width += width + separator_width;
3602 scrollable_columns_that_fit += 1;
3603 } else {
3604 break;
3605 }
3606 }
3607
3608 let new_end = new_scroll_offset + scrollable_columns_that_fit;
3610 self.viewport_cols = new_scroll_offset..new_end;
3611 self.cache_dirty = true; debug!(target: "viewport_manager",
3614 "Adjusted viewport for visual column {}: offset {}→{} (viewport: {:?})",
3615 visual_column, old_scroll_offset, new_scroll_offset, self.viewport_cols);
3616
3617 return true;
3618 }
3619
3620 false
3621 }
3622
3623 fn calculate_visible_column_indices_with_offset(
3626 &mut self,
3627 available_width: u16,
3628 scroll_offset: usize,
3629 ) -> Vec<usize> {
3630 let original_viewport = self.viewport_cols.clone();
3632 let total_visual_columns = self.dataview.get_display_columns().len();
3633 self.viewport_cols = scroll_offset..(scroll_offset + 50).min(total_visual_columns);
3634
3635 let visible_columns = self.calculate_visible_column_indices(available_width);
3636
3637 self.viewport_cols = original_viewport;
3639
3640 visible_columns
3641 }
3642
3643 fn calculate_scroll_offset_for_visual_column(&mut self, visual_column: usize) -> usize {
3646 debug!(target: "viewport_manager",
3647 "=== calculate_scroll_offset_for_visual_column ENTRY ===");
3648 debug!(target: "viewport_manager",
3649 "visual_column={}, current_viewport={:?}", visual_column, self.viewport_cols);
3650
3651 let pinned_count = self.dataview.get_pinned_columns().len();
3652 debug!(target: "viewport_manager",
3653 "pinned_count={}", pinned_count);
3654
3655 if visual_column < pinned_count {
3657 debug!(target: "viewport_manager",
3658 "Visual column {} is pinned, returning current offset {}",
3659 visual_column, self.viewport_cols.start);
3660 return self.viewport_cols.start; }
3662
3663 let scrollable_column = visual_column - pinned_count;
3665 debug!(target: "viewport_manager",
3666 "Converted to scrollable_column={}", scrollable_column);
3667
3668 let current_scroll_offset = self.viewport_cols.start;
3669 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3670
3671 let display_columns = self.dataview.get_display_columns();
3673 let mut pinned_width = 0u16;
3674 let separator_width = 1u16;
3675
3676 for visual_idx in 0..pinned_count {
3677 if visual_idx < display_columns.len() {
3678 let dt_idx = display_columns[visual_idx];
3679 let width = self.width_calculator.get_column_width(
3680 &self.dataview,
3681 &self.viewport_rows,
3682 dt_idx,
3683 );
3684 pinned_width += width + separator_width;
3685 }
3686 }
3687
3688 let available_for_scrollable = terminal_width.saturating_sub(pinned_width);
3690
3691 debug!(target: "viewport_manager",
3692 "Scroll offset calculation: target_scrollable_col={}, current_offset={}, available_width={}",
3693 scrollable_column, current_scroll_offset, available_for_scrollable);
3694
3695 if scrollable_column < current_scroll_offset {
3697 debug!(target: "viewport_manager", "Column {} is left of viewport, scrolling left to offset {}",
3699 scrollable_column, scrollable_column);
3700 scrollable_column
3701 } else {
3702 debug!(target: "viewport_manager",
3706 "Checking if column {} can be made visible with minimal scrolling from offset {}",
3707 scrollable_column, current_scroll_offset);
3708
3709 let mut test_scroll_offset = current_scroll_offset;
3711 let max_scrollable_columns = display_columns.len().saturating_sub(pinned_count);
3712
3713 while test_scroll_offset <= scrollable_column
3714 && test_scroll_offset < max_scrollable_columns
3715 {
3716 let mut used_width = 0u16;
3717 let mut target_column_fits = false;
3718
3719 for test_scrollable_idx in test_scroll_offset..max_scrollable_columns {
3721 let visual_idx = pinned_count + test_scrollable_idx;
3722 if visual_idx < display_columns.len() {
3723 let dt_idx = display_columns[visual_idx];
3724 let width = self.width_calculator.get_column_width(
3725 &self.dataview,
3726 &self.viewport_rows,
3727 dt_idx,
3728 );
3729
3730 if used_width + width + separator_width <= available_for_scrollable {
3731 used_width += width + separator_width;
3732 if test_scrollable_idx == scrollable_column {
3733 target_column_fits = true;
3734 break; }
3736 } else {
3737 break; }
3739 }
3740 }
3741
3742 debug!(target: "viewport_manager",
3743 "Testing scroll_offset={}: target_fits={}, used_width={}",
3744 test_scroll_offset, target_column_fits, used_width);
3745
3746 if target_column_fits {
3747 debug!(target: "viewport_manager",
3748 "Found minimal scroll offset {} for column {} (current was {})",
3749 test_scroll_offset, scrollable_column, current_scroll_offset);
3750 return test_scroll_offset;
3751 }
3752
3753 test_scroll_offset += 1;
3755 }
3756
3757 debug!(target: "viewport_manager",
3759 "Could not find minimal scroll, placing column {} at scroll offset {}",
3760 scrollable_column, scrollable_column);
3761 scrollable_column
3762 }
3763 }
3764
3765 pub fn goto_line(&mut self, target_row: usize) -> RowNavigationResult {
3767 let total_rows = self.dataview.row_count();
3768
3769 let target_row = target_row.min(total_rows.saturating_sub(1));
3771
3772 let visible_rows = (self.terminal_height as usize).saturating_sub(6);
3774
3775 let centered_scroll_offset = if visible_rows > 0 {
3777 let half_viewport = visible_rows / 2;
3779 if target_row > half_viewport {
3780 (target_row - half_viewport).min(total_rows.saturating_sub(visible_rows))
3782 } else {
3783 0
3785 }
3786 } else {
3787 target_row
3788 };
3789
3790 let old_scroll_offset = self.viewport_rows.start;
3792 self.viewport_rows =
3793 centered_scroll_offset..(centered_scroll_offset + visible_rows).min(total_rows);
3794 let viewport_changed = centered_scroll_offset != old_scroll_offset;
3795
3796 self.crosshair_row = target_row;
3798
3799 let description = format!(
3800 "Jumped to row {} (centered at viewport {})",
3801 target_row + 1,
3802 centered_scroll_offset + 1
3803 );
3804
3805 debug!(target: "viewport_manager",
3806 "goto_line: target_row={}, crosshair_row={}, scroll_offset={}→{}, viewport={:?}",
3807 target_row, self.crosshair_row, old_scroll_offset, centered_scroll_offset, self.viewport_rows);
3808
3809 RowNavigationResult {
3810 row_position: target_row,
3811 row_scroll_offset: centered_scroll_offset,
3812 description,
3813 viewport_changed,
3814 }
3815 }
3816
3817 pub fn hide_current_column_with_result(&mut self) -> ColumnOperationResult {
3821 let visual_col_idx = self.get_crosshair_col();
3822 let columns = self.dataview.column_names();
3823
3824 if visual_col_idx >= columns.len() {
3825 return ColumnOperationResult::failure("Invalid column position");
3826 }
3827
3828 let col_name = columns[visual_col_idx].clone();
3829 let visible_count = columns.len();
3830
3831 if visible_count <= 1 {
3833 return ColumnOperationResult::failure("Cannot hide the last visible column");
3834 }
3835
3836 let success = self.hide_column(visual_col_idx);
3838
3839 if success {
3840 let mut result = ColumnOperationResult::success(format!("Column '{col_name}' hidden"));
3841 result.updated_dataview = Some(self.clone_dataview());
3842 result.new_column_position = Some(self.get_crosshair_col());
3843 result.new_viewport = Some(self.viewport_cols.clone());
3844 result
3845 } else {
3846 ColumnOperationResult::failure(format!(
3847 "Cannot hide column '{col_name}' (may be pinned)"
3848 ))
3849 }
3850 }
3851
3852 pub fn unhide_all_columns_with_result(&mut self) -> ColumnOperationResult {
3854 let hidden_columns = self.dataview.get_hidden_column_names();
3855 let count = hidden_columns.len();
3856
3857 if count == 0 {
3858 return ColumnOperationResult::success("No hidden columns");
3859 }
3860
3861 self.unhide_all_columns();
3862
3863 let mut result = ColumnOperationResult::success(format!("Unhidden {count} column(s)"));
3864 result.updated_dataview = Some(self.clone_dataview());
3865 result.affected_count = Some(count);
3866 result.new_viewport = Some(self.viewport_cols.clone());
3867 result
3868 }
3869
3870 pub fn reorder_column_left_with_result(&mut self) -> ColumnOperationResult {
3872 let current_col = self.get_crosshair_col();
3873 let reorder_result = self.reorder_column_left(current_col);
3874
3875 if reorder_result.success {
3876 let mut result = ColumnOperationResult::success(reorder_result.description);
3877 result.updated_dataview = Some(self.clone_dataview());
3878 result.new_column_position = Some(reorder_result.new_column_position);
3879 result.new_viewport = Some(self.viewport_cols.clone());
3880 result
3881 } else {
3882 ColumnOperationResult::failure(reorder_result.description)
3883 }
3884 }
3885
3886 pub fn reorder_column_right_with_result(&mut self) -> ColumnOperationResult {
3888 let current_col = self.get_crosshair_col();
3889 let reorder_result = self.reorder_column_right(current_col);
3890
3891 if reorder_result.success {
3892 let mut result = ColumnOperationResult::success(reorder_result.description);
3893 result.updated_dataview = Some(self.clone_dataview());
3894 result.new_column_position = Some(reorder_result.new_column_position);
3895 result.new_viewport = Some(self.viewport_cols.clone());
3896 result
3897 } else {
3898 ColumnOperationResult::failure(reorder_result.description)
3899 }
3900 }
3901
3902 pub fn calculate_viewport_column_widths(
3907 &mut self,
3908 viewport_start: usize,
3909 viewport_end: usize,
3910 compact_mode: bool,
3911 ) -> Vec<u16> {
3912 let headers = self.dataview.column_names();
3913 let mut widths = Vec::with_capacity(headers.len());
3914
3915 let min_width = if compact_mode { 4 } else { 6 };
3917 let padding = if compact_mode { 1 } else { 2 };
3918
3919 let available_width = self.terminal_width.saturating_sub(10) as usize;
3921 let visible_cols = headers.len().min(12); let dynamic_max = if visible_cols > 0 {
3925 (available_width / visible_cols).max(30).min(80)
3926 } else {
3927 30
3928 };
3929
3930 let max_width = if compact_mode {
3931 dynamic_max.min(40)
3932 } else {
3933 dynamic_max
3934 };
3935
3936 let mut rows_to_check = Vec::new();
3938 let source_table = self.dataview.source();
3939 for i in viewport_start..viewport_end.min(source_table.row_count()) {
3940 if let Some(row_strings) = source_table.get_row_as_strings(i) {
3941 rows_to_check.push(row_strings);
3942 }
3943 }
3944
3945 for (col_idx, header) in headers.iter().enumerate() {
3947 let mut max_col_width = header.len();
3949
3950 for row in &rows_to_check {
3952 if let Some(value) = row.get(col_idx) {
3953 let display_value = if value.is_empty() {
3954 "NULL"
3955 } else {
3956 value.as_str()
3957 };
3958 max_col_width = max_col_width.max(display_value.len());
3959 }
3960 }
3961
3962 let width = (max_col_width + padding).clamp(min_width, max_width) as u16;
3964 widths.push(width);
3965 }
3966
3967 widths
3968 }
3969
3970 pub fn calculate_optimal_column_widths(&mut self) -> Vec<u16> {
3973 self.width_calculator.calculate_with_terminal_width(
3975 &self.dataview,
3976 &self.viewport_rows,
3977 self.terminal_width,
3978 );
3979
3980 let col_count = self.dataview.column_count();
3982 let mut widths = Vec::with_capacity(col_count);
3983 for idx in 0..col_count {
3984 widths.push(self.width_calculator.get_column_width(
3985 &self.dataview,
3986 &self.viewport_rows,
3987 idx,
3988 ));
3989 }
3990 widths
3991 }
3992
3993 pub fn ensure_column_visible(&mut self, column_index: usize, available_width: u16) {
3995 debug!(target: "viewport_manager", "ensure_column_visible: column_index={}, available_width={}", column_index, available_width);
3996
3997 let total_columns = self.dataview.get_display_columns().len();
3998
3999 if column_index >= total_columns {
4001 debug!(target: "viewport_manager", "Column index {} out of range (max {})", column_index, total_columns.saturating_sub(1));
4002 return;
4003 }
4004
4005 let visible_columns = self.calculate_visible_column_indices(available_width);
4007 let dt_columns = self.dataview.get_display_columns();
4008
4009 if let Some(&dt_index) = dt_columns.get(column_index) {
4011 if visible_columns.contains(&dt_index) {
4012 debug!(target: "viewport_manager", "Column {} already visible", column_index);
4013 return;
4014 }
4015 }
4016
4017 if self.set_current_column(column_index) {
4020 self.crosshair_col = column_index;
4021 debug!(target: "viewport_manager", "Ensured column {} is visible and set crosshair", column_index);
4022 } else {
4023 debug!(target: "viewport_manager", "Failed to make column {} visible", column_index);
4024 }
4025 }
4026
4027 pub fn reorder_column(&mut self, from_index: usize, to_index: usize) -> bool {
4029 debug!(target: "viewport_manager", "reorder_column: from_index={}, to_index={}", from_index, to_index);
4030
4031 if from_index == to_index {
4032 return true; }
4034
4035 let mut new_dataview = (*self.dataview).clone();
4037
4038 let mut current_pos = from_index;
4039 let mut success = true;
4040
4041 if from_index < to_index {
4043 while current_pos < to_index && success {
4045 success = new_dataview.move_column_right(current_pos);
4046 if success {
4047 current_pos += 1;
4048 }
4049 }
4050 } else {
4051 while current_pos > to_index && success {
4053 success = new_dataview.move_column_left(current_pos);
4054 if success {
4055 current_pos -= 1;
4056 }
4057 }
4058 }
4059
4060 if success {
4061 self.dataview = Arc::new(new_dataview);
4063 self.invalidate_cache(); debug!(target: "viewport_manager", "Column moved from {} to {} successfully", from_index, to_index);
4066 } else {
4067 debug!(target: "viewport_manager", "Failed to move column from {} to {}", from_index, to_index);
4068 }
4069
4070 success
4071 }
4072
4073 pub fn calculate_column_widths(&mut self, available_width: u16) -> Vec<u16> {
4076 let _visible_indices = self.calculate_visible_column_indices(available_width);
4078
4079 self.get_column_widths().to_vec()
4081 }
4082}
4083
4084#[derive(Debug, Clone)]
4086pub struct ViewportEfficiency {
4087 pub available_width: u16,
4088 pub used_width: u16,
4089 pub wasted_space: u16,
4090 pub efficiency_percent: u8,
4091 pub visible_columns: usize,
4092 pub column_widths: Vec<u16>,
4093 pub next_column_width: Option<u16>, pub columns_that_could_fit: Vec<(usize, u16)>, }
4096
4097impl ViewportEfficiency {
4098 #[must_use]
4100 pub fn to_status_string(&self) -> String {
4101 format!(
4102 "Viewport: {}w used of {}w ({}% efficient, {} cols, {}w wasted)",
4103 self.used_width,
4104 self.available_width,
4105 self.efficiency_percent,
4106 self.visible_columns,
4107 self.wasted_space
4108 )
4109 }
4110
4111 #[must_use]
4113 pub fn to_debug_string(&self) -> String {
4114 let avg_width = if self.column_widths.is_empty() {
4115 0
4116 } else {
4117 self.column_widths.iter().sum::<u16>() / self.column_widths.len() as u16
4118 };
4119
4120 let mut efficiency_analysis = String::new();
4122 if let Some(next_width) = self.next_column_width {
4123 efficiency_analysis.push_str(&format!(
4124 "\n\n Next column needs: {next_width}w (+1 separator)"
4125 ));
4126 if next_width < self.wasted_space {
4127 efficiency_analysis.push_str(" ✓ FITS!");
4128 } else {
4129 efficiency_analysis.push_str(&format!(" ✗ Too wide (have {}w)", self.wasted_space));
4130 }
4131 }
4132
4133 if !self.columns_that_could_fit.is_empty() {
4134 efficiency_analysis.push_str(&format!(
4135 "\n Columns that COULD fit in {}w:",
4136 self.wasted_space
4137 ));
4138 for (idx, width) in
4139 &self.columns_that_could_fit[..self.columns_that_could_fit.len().min(5)]
4140 {
4141 efficiency_analysis.push_str(&format!("\n - Column {idx}: {width}w"));
4142 }
4143 if self.columns_that_could_fit.len() > 5 {
4144 efficiency_analysis.push_str(&format!(
4145 "\n ... and {} more",
4146 self.columns_that_could_fit.len() - 5
4147 ));
4148 }
4149 }
4150
4151 efficiency_analysis.push_str("\n\n Hypothetical efficiencies:");
4153 for extra in 1..=3 {
4154 let hypothetical_used =
4155 self.used_width + (extra * (avg_width + 1)).min(self.wasted_space);
4156 let hypothetical_eff =
4157 ((f32::from(hypothetical_used) / f32::from(self.available_width)) * 100.0) as u8;
4158 let hypothetical_wasted = self.available_width.saturating_sub(hypothetical_used);
4159 efficiency_analysis.push_str(&format!(
4160 "\n +{extra} cols ({avg_width}w each): {hypothetical_eff}% efficiency, {hypothetical_wasted}w wasted"
4161 ));
4162 }
4163
4164 format!(
4165 "Viewport Efficiency:\n Available: {}w\n Used: {}w\n Wasted: {}w\n Efficiency: {}%\n Columns: {} visible\n Widths: {:?}\n Avg Width: {}w{}",
4166 self.available_width,
4167 self.used_width,
4168 self.wasted_space,
4169 self.efficiency_percent,
4170 self.visible_columns,
4171 self.column_widths.clone(),
4172 avg_width,
4173 efficiency_analysis
4174 )
4175 }
4176}
4177
4178#[cfg(test)]
4179mod tests {
4180 use super::*;
4181 use crate::data::datatable::{DataColumn, DataRow, DataTable, DataValue};
4182
4183 fn create_test_dataview() -> Arc<DataView> {
4184 let mut table = DataTable::new("test");
4185 table.add_column(DataColumn::new("id"));
4186 table.add_column(DataColumn::new("name"));
4187 table.add_column(DataColumn::new("amount"));
4188
4189 for i in 0..100 {
4190 table
4191 .add_row(DataRow::new(vec![
4192 DataValue::Integer(i),
4193 DataValue::String(format!("Item {i}")),
4194 DataValue::Float(i as f64 * 10.5),
4195 ]))
4196 .unwrap();
4197 }
4198
4199 Arc::new(DataView::new(Arc::new(table)))
4200 }
4201
4202 #[test]
4203 fn test_viewport_basic() {
4204 let dataview = create_test_dataview();
4205 let mut viewport = ViewportManager::new(dataview);
4206
4207 viewport.set_viewport(0, 0, 80, 24);
4208
4209 assert_eq!(viewport.viewport_rows(), 0..24);
4210 assert_eq!(viewport.viewport_cols(), 0..3);
4211
4212 let visible_rows = viewport.get_visible_rows();
4213 assert_eq!(visible_rows.len(), 24);
4214 }
4215
4216 #[test]
4217 fn test_column_width_calculation() {
4218 let dataview = create_test_dataview();
4219 let mut viewport = ViewportManager::new(dataview);
4220
4221 viewport.set_viewport(0, 0, 80, 10);
4222
4223 let widths = viewport.get_column_widths();
4224 assert_eq!(widths.len(), 3);
4225
4226 assert!(widths[0] < 10);
4228 assert!(widths[1] > widths[0]);
4230 }
4231
4232 #[test]
4233 fn test_viewport_scrolling() {
4234 let dataview = create_test_dataview();
4235 let mut viewport = ViewportManager::new(dataview);
4236
4237 viewport.set_viewport(0, 0, 80, 24);
4238 viewport.scroll_by(10, 0);
4239
4240 assert_eq!(viewport.viewport_rows(), 10..34);
4241
4242 viewport.scroll_by(-5, 1);
4243 assert_eq!(viewport.viewport_rows(), 5..29);
4244 assert_eq!(viewport.viewport_cols(), 1..3);
4245 }
4246
4247 #[test]
4250 fn test_navigate_to_last_and_first_column() {
4251 let dataview = create_test_dataview();
4252 let mut vm = ViewportManager::new(dataview);
4253 vm.update_terminal_size(120, 40);
4254
4255 let result = vm.navigate_to_last_column();
4257 assert_eq!(vm.get_crosshair_col(), 2); assert_eq!(result.column_position, 2);
4259
4260 let result = vm.navigate_to_first_column();
4262 assert_eq!(vm.get_crosshair_col(), 0);
4263 assert_eq!(result.column_position, 0);
4264 }
4265
4266 #[test]
4267 fn test_column_reorder_right_with_crosshair() {
4268 let dataview = create_test_dataview();
4269 let mut vm = ViewportManager::new(dataview);
4270 vm.update_terminal_size(120, 40);
4271
4272 vm.crosshair_col = 0;
4274
4275 let result = vm.reorder_column_right(0);
4277 assert!(result.success);
4278 assert_eq!(result.new_column_position, 1);
4279 assert_eq!(vm.get_crosshair_col(), 1); let headers = vm.dataview.column_names();
4283 assert_eq!(headers[0], "name"); assert_eq!(headers[1], "id"); }
4286
4287 #[test]
4288 fn test_column_reorder_left_with_crosshair() {
4289 let dataview = create_test_dataview();
4290 let mut vm = ViewportManager::new(dataview);
4291 vm.update_terminal_size(120, 40);
4292
4293 vm.crosshair_col = 1;
4295
4296 let result = vm.reorder_column_left(1);
4298 assert!(result.success);
4299 assert_eq!(result.new_column_position, 0);
4300 assert_eq!(vm.get_crosshair_col(), 0); }
4302
4303 #[test]
4304 fn test_hide_column_adjusts_crosshair() {
4305 let dataview = create_test_dataview();
4306 let mut vm = ViewportManager::new(dataview);
4307 vm.update_terminal_size(120, 40);
4308
4309 vm.crosshair_col = 1; let success = vm.hide_column(1);
4312 assert!(success);
4313 assert_eq!(vm.get_crosshair_col(), 1);
4315 assert_eq!(vm.dataview.column_count(), 2); vm.crosshair_col = 1; let success = vm.hide_column(1);
4320 assert!(success);
4321 assert_eq!(vm.get_crosshair_col(), 0); assert_eq!(vm.dataview.column_count(), 1); }
4324
4325 #[test]
4326 fn test_goto_line_centers_viewport() {
4327 let dataview = create_test_dataview();
4328 let mut vm = ViewportManager::new(dataview);
4329 vm.update_terminal_size(120, 40);
4330
4331 let result = vm.goto_line(50);
4333 assert_eq!(result.row_position, 50);
4334 assert_eq!(vm.get_crosshair_row(), 50);
4335
4336 let visible_rows = 34; let expected_offset = 50 - (visible_rows / 2);
4339 assert_eq!(result.row_scroll_offset, expected_offset);
4340 }
4341
4342 #[test]
4343 fn test_page_navigation() {
4344 let dataview = create_test_dataview();
4345 let mut vm = ViewportManager::new(dataview);
4346 vm.update_terminal_size(120, 40);
4347
4348 let initial_row = vm.get_crosshair_row();
4350 let result = vm.page_down();
4351 assert!(result.row_position > initial_row);
4352 assert_eq!(vm.get_crosshair_row(), result.row_position);
4353
4354 vm.page_down(); vm.page_down();
4357 let prev_position = vm.get_crosshair_row();
4358 let result = vm.page_up();
4359 assert!(result.row_position < prev_position); }
4361
4362 #[test]
4363 fn test_cursor_lock_mode() {
4364 let dataview = create_test_dataview();
4365 let mut vm = ViewportManager::new(dataview);
4366 vm.update_terminal_size(120, 40);
4367
4368 vm.toggle_cursor_lock();
4370 assert!(vm.is_cursor_locked());
4371
4372 let initial_viewport_position = vm.get_crosshair_row() - vm.viewport_rows.start;
4374 let result = vm.navigate_row_down();
4375
4376 if result.viewport_changed {
4378 let new_viewport_position = vm.get_crosshair_row() - vm.viewport_rows.start;
4379 assert_eq!(initial_viewport_position, new_viewport_position);
4380 }
4381 }
4382
4383 #[test]
4384 fn test_viewport_lock_prevents_scrolling() {
4385 let dataview = create_test_dataview();
4386 let mut vm = ViewportManager::new(dataview);
4387 vm.update_terminal_size(120, 40);
4388
4389 vm.toggle_viewport_lock();
4391 assert!(vm.is_viewport_locked());
4392
4393 let initial_viewport = vm.viewport_rows.clone();
4395 let result = vm.navigate_row_down();
4396
4397 assert_eq!(vm.viewport_rows, initial_viewport);
4399 assert!(!result.viewport_changed);
4401 }
4402
4403 #[test]
4404 fn test_h_m_l_viewport_navigation() {
4405 let dataview = create_test_dataview();
4406 let mut vm = ViewportManager::new(dataview);
4407 vm.update_terminal_size(120, 40);
4408
4409 for _ in 0..20 {
4411 vm.navigate_row_down();
4412 }
4413
4414 let result = vm.navigate_to_viewport_top();
4416 assert_eq!(vm.get_crosshair_row(), vm.viewport_rows.start);
4417
4418 let result = vm.navigate_to_viewport_bottom();
4420 assert_eq!(vm.get_crosshair_row(), vm.viewport_rows.end - 1);
4421
4422 let result = vm.navigate_to_viewport_middle();
4424 let expected_middle =
4425 vm.viewport_rows.start + (vm.viewport_rows.end - vm.viewport_rows.start) / 2;
4426 assert_eq!(vm.get_crosshair_row(), expected_middle);
4427 }
4428
4429 #[test]
4430 fn test_out_of_order_column_navigation() {
4431 let mut table = DataTable::new("test");
4433 for i in 0..12 {
4434 table.add_column(DataColumn::new(format!("col{i}")));
4435 }
4436
4437 for row in 0..10 {
4439 let mut values = Vec::new();
4440 for col in 0..12 {
4441 values.push(DataValue::String(format!("r{row}c{col}")));
4442 }
4443 table.add_row(DataRow::new(values)).unwrap();
4444 }
4445
4446 let dataview =
4450 DataView::new(Arc::new(table)).with_columns(vec![11, 0, 5, 3, 8, 1, 10, 2, 7, 4, 9, 6]);
4451
4452 let mut vm = ViewportManager::new(Arc::new(dataview));
4453 vm.update_terminal_size(200, 40); let column_names = vm.dataview.column_names();
4457 assert_eq!(
4458 column_names[0], "col11",
4459 "First visual column should be col11"
4460 );
4461 assert_eq!(
4462 column_names[1], "col0",
4463 "Second visual column should be col0"
4464 );
4465 assert_eq!(
4466 column_names[2], "col5",
4467 "Third visual column should be col5"
4468 );
4469
4470 vm.crosshair_col = 0;
4472
4473 let mut visual_positions = vec![0];
4475 let mut datatable_positions = vec![];
4476
4477 let display_cols = vm.dataview.get_display_columns();
4479 datatable_positions.push(display_cols[0]);
4480
4481 for i in 0..11 {
4483 let current_visual = vm.get_crosshair_col();
4484 let result = vm.navigate_column_right(current_visual);
4485
4486 let new_visual = vm.get_crosshair_col();
4488 assert_eq!(
4489 new_visual,
4490 current_visual + 1,
4491 "Crosshair should move from visual position {} to {}, but got {}",
4492 current_visual,
4493 current_visual + 1,
4494 new_visual
4495 );
4496
4497 visual_positions.push(new_visual);
4498 let display_cols = vm.dataview.get_display_columns();
4500 datatable_positions.push(display_cols[new_visual]);
4501 }
4502
4503 assert_eq!(
4505 visual_positions,
4506 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
4507 "Crosshair should move through visual positions sequentially"
4508 );
4509
4510 assert_eq!(
4512 datatable_positions,
4513 vec![11, 0, 5, 3, 8, 1, 10, 2, 7, 4, 9, 6],
4514 "DataTable indices should match our column selection order"
4515 );
4516
4517 for _i in (0..11).rev() {
4519 let current_visual = vm.get_crosshair_col();
4520 let _result = vm.navigate_column_left(current_visual);
4521
4522 let new_visual = vm.get_crosshair_col();
4524 assert_eq!(
4525 new_visual,
4526 current_visual - 1,
4527 "Crosshair should move from visual position {} to {}, but got {}",
4528 current_visual,
4529 current_visual - 1,
4530 new_visual
4531 );
4532 }
4533
4534 assert_eq!(
4536 vm.get_crosshair_col(),
4537 0,
4538 "Should be back at first visual column"
4539 );
4540
4541 vm.hide_column(2); vm.crosshair_col = 0;
4546 let _result1 = vm.navigate_column_right(0);
4547 assert_eq!(vm.get_crosshair_col(), 1, "Should be at visual position 1");
4548
4549 let _result2 = vm.navigate_column_right(1);
4550 assert_eq!(
4551 vm.get_crosshair_col(),
4552 2,
4553 "Should be at visual position 2 after hiding"
4554 );
4555
4556 let visible_cols = vm.dataview.column_names();
4558 assert_eq!(
4559 visible_cols[2], "col3",
4560 "Column at position 2 should be col3 after hiding col5"
4561 );
4562 }
4563}