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;
113
114const TABLE_BORDER_WIDTH: u16 = 4;
116
117pub struct ViewportManager {
119 dataview: Arc<DataView>,
121
122 viewport_rows: Range<usize>,
124 viewport_cols: Range<usize>,
125
126 terminal_width: u16,
128 terminal_height: u16,
129
130 width_calculator: ColumnWidthCalculator,
132
133 visible_row_cache: Vec<usize>,
135
136 cache_signature: u64,
138
139 cache_dirty: bool,
141
142 crosshair_row: usize,
145 crosshair_col: usize,
146
147 cursor_lock: bool,
149 cursor_lock_position: Option<usize>,
151
152 viewport_lock: bool,
154 viewport_lock_boundaries: Option<std::ops::Range<usize>>,
156}
157
158impl ViewportManager {
159 #[must_use]
161 pub fn get_viewport_range(&self) -> std::ops::Range<usize> {
162 self.viewport_cols.clone()
163 }
164
165 #[must_use]
167 pub fn get_viewport_rows(&self) -> std::ops::Range<usize> {
168 self.viewport_rows.clone()
169 }
170
171 pub fn set_crosshair(&mut self, row: usize, col: usize) {
173 self.crosshair_row = row;
174 self.crosshair_col = col;
175 debug!(target: "viewport_manager",
176 "Crosshair set to visual position: row={}, col={}", row, col);
177 }
178
179 pub fn set_crosshair_row(&mut self, row: usize) {
181 let total_rows = self.dataview.row_count();
182
183 let clamped_row = row.min(total_rows.saturating_sub(1));
185 self.crosshair_row = clamped_row;
186
187 if self.viewport_lock {
189 debug!(target: "viewport_manager",
190 "Crosshair row set to: {} (viewport locked, no scroll adjustment)",
191 clamped_row);
192 return;
193 }
194
195 let viewport_height = self.viewport_rows.len();
197 let mut viewport_changed = false;
198
199 if clamped_row < self.viewport_rows.start {
200 self.viewport_rows = clamped_row..(clamped_row + viewport_height).min(total_rows);
202 viewport_changed = true;
203 } else if clamped_row >= self.viewport_rows.end {
204 let new_start = clamped_row.saturating_sub(viewport_height.saturating_sub(1));
206 self.viewport_rows = new_start..(new_start + viewport_height).min(total_rows);
207 viewport_changed = true;
208 }
209
210 if viewport_changed {
211 debug!(target: "viewport_manager",
212 "Crosshair row set to: {}, adjusted viewport to: {:?}",
213 clamped_row, self.viewport_rows);
214 } else {
215 debug!(target: "viewport_manager",
216 "Crosshair row set to: {}", clamped_row);
217 }
218 }
219
220 pub fn set_crosshair_column(&mut self, col: usize) {
222 let total_columns = self.dataview.get_display_columns().len();
223
224 let clamped_col = col.min(total_columns.saturating_sub(1));
226 self.crosshair_col = clamped_col;
227
228 if self.viewport_lock {
230 debug!(target: "viewport_manager",
231 "Crosshair column set to: {} (viewport locked, no scroll adjustment)",
232 clamped_col);
233 return;
234 }
235
236 let terminal_width = self.terminal_width.saturating_sub(4); if self.set_current_column(clamped_col) {
239 debug!(target: "viewport_manager",
240 "Crosshair column set to: {} with viewport adjustment", clamped_col);
241 } else {
242 debug!(target: "viewport_manager",
243 "Crosshair column set to: {}", clamped_col);
244 }
245 }
246
247 #[must_use]
249 pub fn get_crosshair_col(&self) -> usize {
250 self.crosshair_col
251 }
252
253 #[must_use]
255 pub fn get_crosshair_row(&self) -> usize {
256 self.crosshair_row
257 }
258
259 #[must_use]
261 pub fn get_selected_row(&self) -> usize {
262 self.crosshair_row
263 }
264
265 #[must_use]
267 pub fn get_selected_column(&self) -> usize {
268 self.crosshair_col
269 }
270
271 #[must_use]
273 pub fn get_crosshair_position(&self) -> (usize, usize) {
274 (self.crosshair_row, self.crosshair_col)
275 }
276
277 #[must_use]
279 pub fn get_scroll_offset(&self) -> (usize, usize) {
280 (self.viewport_rows.start, self.viewport_cols.start)
281 }
282
283 pub fn set_scroll_offset(&mut self, row_offset: usize, col_offset: usize) {
285 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
286 let viewport_width = self.viewport_cols.end - self.viewport_cols.start;
287
288 self.viewport_rows = row_offset..(row_offset + viewport_height);
290 self.viewport_cols = col_offset..(col_offset + viewport_width);
291
292 if self.crosshair_row < self.viewport_rows.start {
294 self.crosshair_row = self.viewport_rows.start;
295 } else if self.crosshair_row >= self.viewport_rows.end {
296 self.crosshair_row = self.viewport_rows.end.saturating_sub(1);
297 }
298
299 if self.crosshair_col < self.viewport_cols.start {
300 self.crosshair_col = self.viewport_cols.start;
301 } else if self.crosshair_col >= self.viewport_cols.end {
302 self.crosshair_col = self.viewport_cols.end.saturating_sub(1);
303 }
304
305 self.cache_dirty = true;
306 }
307
308 #[must_use]
311 pub fn get_crosshair_viewport_position(&self) -> Option<(usize, usize)> {
312 if self.crosshair_row < self.viewport_rows.start
315 || self.crosshair_row >= self.viewport_rows.end
316 {
317 return None;
318 }
319
320 let pinned_count = self.dataview.get_pinned_columns().len();
322
323 if self.crosshair_col < pinned_count {
325 return Some((
326 self.crosshair_row - self.viewport_rows.start,
327 self.crosshair_col, ));
329 }
330
331 let scrollable_col = self.crosshair_col - pinned_count;
334 if scrollable_col >= self.viewport_cols.start && scrollable_col < self.viewport_cols.end {
335 let visual_col_in_viewport = pinned_count + (scrollable_col - self.viewport_cols.start);
338 return Some((
339 self.crosshair_row - self.viewport_rows.start,
340 visual_col_in_viewport,
341 ));
342 }
343
344 None
345 }
346
347 pub fn navigate_row_up(&mut self) -> RowNavigationResult {
349 let total_rows = self.dataview.row_count();
350
351 if self.viewport_lock {
353 debug!(target: "viewport_manager",
354 "navigate_row_up: Viewport locked, crosshair={}, viewport={:?}",
355 self.crosshair_row, self.viewport_rows);
356 if self.crosshair_row > self.viewport_rows.start {
358 self.crosshair_row -= 1;
359 return RowNavigationResult {
360 row_position: self.crosshair_row,
361 row_scroll_offset: self.viewport_rows.start,
362 description: "Moved within locked viewport".to_string(),
363 viewport_changed: false,
364 };
365 }
366 return RowNavigationResult {
368 row_position: self.crosshair_row,
369 row_scroll_offset: self.viewport_rows.start,
370 description: "Moved within locked viewport".to_string(),
371 viewport_changed: false,
372 };
373 }
374
375 if self.cursor_lock {
377 if let Some(lock_position) = self.cursor_lock_position {
378 if self.viewport_rows.start == 0 {
380 return RowNavigationResult {
382 row_position: self.crosshair_row,
383 row_scroll_offset: self.viewport_rows.start,
384 description: "At top of data".to_string(),
385 viewport_changed: false,
386 };
387 }
388
389 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
390 let new_viewport_start = self.viewport_rows.start.saturating_sub(1);
391
392 self.viewport_rows =
394 new_viewport_start..(new_viewport_start + viewport_height).min(total_rows);
395
396 self.crosshair_row = (self.viewport_rows.start + lock_position)
398 .min(self.viewport_rows.end.saturating_sub(1));
399
400 return RowNavigationResult {
401 row_position: self.crosshair_row,
402 row_scroll_offset: self.viewport_rows.start,
403 description: format!(
404 "Scrolled up (locked at viewport row {})",
405 lock_position + 1
406 ),
407 viewport_changed: true,
408 };
409 }
410 }
411
412 if self.crosshair_row == 0 {
415 return RowNavigationResult {
417 row_position: 0,
418 row_scroll_offset: self.viewport_rows.start,
419 description: "Already at first row".to_string(),
420 viewport_changed: false,
421 };
422 }
423
424 let new_row = self.crosshair_row - 1;
425 self.crosshair_row = new_row;
426
427 let viewport_changed = if new_row < self.viewport_rows.start {
429 self.viewport_rows = new_row..self.viewport_rows.end.saturating_sub(1);
430 true
431 } else {
432 false
433 };
434
435 RowNavigationResult {
436 row_position: new_row,
437 row_scroll_offset: self.viewport_rows.start,
438 description: format!("Move to row {}", new_row + 1),
439 viewport_changed,
440 }
441 }
442
443 pub fn navigate_row_down(&mut self) -> RowNavigationResult {
445 let total_rows = self.dataview.row_count();
446
447 if self.viewport_lock {
449 debug!(target: "viewport_manager",
450 "navigate_row_down: Viewport locked, crosshair={}, viewport={:?}",
451 self.crosshair_row, self.viewport_rows);
452 if self.crosshair_row < self.viewport_rows.end - 1
454 && self.crosshair_row < total_rows - 1
455 {
456 self.crosshair_row += 1;
457 return RowNavigationResult {
458 row_position: self.crosshair_row,
459 row_scroll_offset: self.viewport_rows.start,
460 description: "Moved within locked viewport".to_string(),
461 viewport_changed: false,
462 };
463 }
464 return RowNavigationResult {
466 row_position: self.crosshair_row,
467 row_scroll_offset: self.viewport_rows.start,
468 description: "Moved within locked viewport".to_string(),
469 viewport_changed: false,
470 };
471 }
472
473 if self.cursor_lock {
475 if let Some(lock_position) = self.cursor_lock_position {
476 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
478 let new_viewport_start =
479 (self.viewport_rows.start + 1).min(total_rows.saturating_sub(viewport_height));
480
481 if new_viewport_start == self.viewport_rows.start {
482 return RowNavigationResult {
484 row_position: self.crosshair_row,
485 row_scroll_offset: self.viewport_rows.start,
486 description: "At bottom of data".to_string(),
487 viewport_changed: false,
488 };
489 }
490
491 self.viewport_rows =
493 new_viewport_start..(new_viewport_start + viewport_height).min(total_rows);
494
495 self.crosshair_row = (self.viewport_rows.start + lock_position)
497 .min(self.viewport_rows.end.saturating_sub(1));
498
499 return RowNavigationResult {
500 row_position: self.crosshair_row,
501 row_scroll_offset: self.viewport_rows.start,
502 description: format!(
503 "Scrolled down (locked at viewport row {})",
504 lock_position + 1
505 ),
506 viewport_changed: true,
507 };
508 }
509 }
510
511 if self.crosshair_row + 1 >= total_rows {
514 let last_row = total_rows.saturating_sub(1);
516 return RowNavigationResult {
517 row_position: last_row,
518 row_scroll_offset: self.viewport_rows.start,
519 description: "Already at last row".to_string(),
520 viewport_changed: false,
521 };
522 }
523
524 let new_row = self.crosshair_row + 1;
525 self.crosshair_row = new_row;
526
527 let viewport_changed = if new_row >= self.viewport_rows.end {
530 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
532 self.viewport_rows = (new_row + 1).saturating_sub(viewport_height)..(new_row + 1);
533 true
534 } else {
535 false
536 };
537
538 RowNavigationResult {
539 row_position: new_row,
540 row_scroll_offset: self.viewport_rows.start,
541 description: format!("Move to row {}", new_row + 1),
542 viewport_changed,
543 }
544 }
545
546 #[must_use]
548 pub fn new(dataview: Arc<DataView>) -> Self {
549 let display_columns = dataview.get_display_columns();
551 let visible_col_count = display_columns.len();
552 let total_col_count = dataview.source().column_count(); let total_rows = dataview.row_count();
554
555 let initial_viewport_cols = if visible_col_count > 0 {
557 0..visible_col_count.min(20) } else {
559 0..0
560 };
561
562 let default_visible_rows = 50usize; let initial_viewport_rows = if total_rows > 0 {
566 0..total_rows.min(default_visible_rows)
567 } else {
568 0..0
569 };
570
571 Self {
572 dataview,
573 viewport_rows: initial_viewport_rows,
574 viewport_cols: initial_viewport_cols,
575 terminal_width: 80,
576 terminal_height: 24,
577 width_calculator: ColumnWidthCalculator::new(),
578 visible_row_cache: Vec::new(),
579 cache_signature: 0,
580 cache_dirty: true,
581 crosshair_row: 0,
582 crosshair_col: 0,
583 cursor_lock: false,
584 cursor_lock_position: None,
585 viewport_lock: false,
586 viewport_lock_boundaries: None,
587 }
588 }
589
590 pub fn set_dataview(&mut self, dataview: Arc<DataView>) {
592 self.dataview = dataview;
593 self.invalidate_cache();
594 }
595
596 pub fn reset_crosshair(&mut self) {
598 self.crosshair_row = 0;
599 self.crosshair_col = 0;
600 self.cursor_lock = false;
601 self.cursor_lock_position = None;
602 }
603
604 #[must_use]
606 pub fn get_packing_mode(&self) -> ColumnPackingMode {
607 self.width_calculator.get_packing_mode()
608 }
609
610 pub fn set_packing_mode(&mut self, mode: ColumnPackingMode) {
612 self.width_calculator.set_packing_mode(mode);
613 self.invalidate_cache();
614 }
615
616 pub fn cycle_packing_mode(&mut self) -> ColumnPackingMode {
618 self.width_calculator.cycle_packing_mode();
619 self.invalidate_cache();
620 self.width_calculator.get_packing_mode()
621 }
622
623 pub fn set_viewport(&mut self, row_offset: usize, col_offset: usize, width: u16, height: u16) {
625 let new_rows = row_offset
626 ..row_offset
627 .saturating_add(height as usize)
628 .min(self.dataview.row_count());
629
630 let display_columns = self.dataview.get_display_columns();
633 let visual_column_count = display_columns.len();
634
635 let columns_that_fit = self.calculate_columns_that_fit(col_offset, width);
637 let new_cols = col_offset
638 ..col_offset
639 .saturating_add(columns_that_fit)
640 .min(visual_column_count);
641
642 if new_rows != self.viewport_rows || new_cols != self.viewport_cols {
644 self.viewport_rows = new_rows;
645 self.viewport_cols = new_cols;
646 self.terminal_width = width;
647 self.terminal_height = height;
648 self.cache_dirty = true;
649 }
650 }
651
652 pub fn update_terminal_size(&mut self, terminal_width: u16, terminal_height: u16) -> usize {
655 let visible_rows = (terminal_height as usize).max(10);
658
659 debug!(target: "viewport_manager",
660 "update_terminal_size: terminal_height={}, calculated visible_rows={}",
661 terminal_height, visible_rows
662 );
663
664 let old_viewport = self.viewport_rows.clone();
665
666 self.terminal_width = terminal_width;
668 self.terminal_height = terminal_height;
669
670 let total_rows = self.dataview.row_count();
673
674 let viewport_size = self.viewport_rows.end - self.viewport_rows.start;
676 if viewport_size != visible_rows && total_rows > 0 {
677 if self.crosshair_row < self.viewport_rows.start {
680 self.viewport_rows =
682 self.crosshair_row..(self.crosshair_row + visible_rows).min(total_rows);
683 } else if self.crosshair_row >= self.viewport_rows.start + visible_rows {
684 let start = self.crosshair_row.saturating_sub(visible_rows - 1);
686 self.viewport_rows = start..(start + visible_rows).min(total_rows);
687 } else {
688 self.viewport_rows = self.viewport_rows.start
690 ..(self.viewport_rows.start + visible_rows).min(total_rows);
691 }
692 }
693
694 let visible_column_count = self.dataview.get_display_columns().len();
697 if visible_column_count > 0 {
698 let columns_that_fit = self.calculate_columns_that_fit(
702 self.viewport_cols.start,
703 terminal_width.saturating_sub(2), );
705
706 let new_col_viewport_end = self
707 .viewport_cols
708 .start
709 .saturating_add(columns_that_fit)
710 .min(visible_column_count);
711
712 let old_col_viewport = self.viewport_cols.clone();
713 self.viewport_cols = self.viewport_cols.start..new_col_viewport_end;
714
715 if old_col_viewport != self.viewport_cols {
716 debug!(target: "viewport_manager",
717 "update_terminal_size - column viewport changed from {:?} to {:?}, terminal_width={}",
718 old_col_viewport, self.viewport_cols, terminal_width
719 );
720 self.cache_dirty = true;
721 }
722 }
723
724 if old_viewport != self.viewport_rows {
725 debug!(target: "navigation",
726 "ViewportManager::update_terminal_size - viewport changed from {:?} to {:?}, crosshair={}, visible_rows={}",
727 old_viewport, self.viewport_rows, self.crosshair_row, visible_rows
728 );
729 }
730
731 visible_rows
732 }
733
734 pub fn scroll_by(&mut self, row_delta: isize, col_delta: isize) {
736 let new_row_start = (self.viewport_rows.start as isize + row_delta).max(0) as usize;
737 let new_col_start = (self.viewport_cols.start as isize + col_delta).max(0) as usize;
738
739 self.set_viewport(
740 new_row_start,
741 new_col_start,
742 self.terminal_width,
743 self.terminal_height,
744 );
745 }
746
747 pub fn get_column_widths(&mut self) -> &[u16] {
749 self.width_calculator
750 .get_all_column_widths(&self.dataview, &self.viewport_rows)
751 }
752
753 pub fn get_column_width(&mut self, col_idx: usize) -> u16 {
755 self.width_calculator
756 .get_column_width(&self.dataview, &self.viewport_rows, col_idx)
757 }
758
759 #[must_use]
761 pub fn get_visible_rows(&self) -> Vec<DataRow> {
762 let mut rows = Vec::with_capacity(self.viewport_rows.len());
763
764 for row_idx in self.viewport_rows.clone() {
765 if let Some(row) = self.dataview.get_row(row_idx) {
766 rows.push(row);
767 }
768 }
769
770 rows
771 }
772
773 #[must_use]
775 pub fn get_visible_row(&self, viewport_row: usize) -> Option<DataRow> {
776 let absolute_row = self.viewport_rows.start + viewport_row;
777 if absolute_row < self.viewport_rows.end {
778 self.dataview.get_row(absolute_row)
779 } else {
780 None
781 }
782 }
783
784 #[must_use]
786 pub fn get_visible_columns(&self) -> Vec<String> {
787 let display_column_names = self.dataview.get_display_column_names();
789
790 let mut visible = Vec::new();
792 for col_idx in self.viewport_cols.clone() {
793 if col_idx < display_column_names.len() {
794 visible.push(display_column_names[col_idx].clone());
795 }
796 }
797
798 visible
799 }
800
801 #[must_use]
803 pub fn viewport_rows(&self) -> Range<usize> {
804 self.viewport_rows.clone()
805 }
806
807 #[must_use]
809 pub fn viewport_cols(&self) -> Range<usize> {
810 self.viewport_cols.clone()
811 }
812
813 #[must_use]
815 pub fn is_row_visible(&self, row_idx: usize) -> bool {
816 self.viewport_rows.contains(&row_idx)
817 }
818
819 #[must_use]
821 pub fn is_column_visible(&self, col_idx: usize) -> bool {
822 self.viewport_cols.contains(&col_idx)
823 }
824
825 #[must_use]
827 pub fn total_rows(&self) -> usize {
828 self.dataview.row_count()
829 }
830
831 #[must_use]
833 pub fn total_columns(&self) -> usize {
834 self.dataview.column_count()
835 }
836
837 #[must_use]
839 pub fn get_terminal_width(&self) -> u16 {
840 self.terminal_width
841 }
842
843 #[must_use]
845 pub fn get_terminal_height(&self) -> usize {
846 self.terminal_height as usize
847 }
848
849 pub fn invalidate_cache(&mut self) {
851 self.cache_dirty = true;
852 self.width_calculator.mark_dirty();
853 }
854
855 pub fn calculate_visible_column_indices(&mut self, available_width: u16) -> Vec<usize> {
859 let display_columns = self.dataview.get_display_columns();
863 let total_visual_columns = display_columns.len();
864
865 if total_visual_columns == 0 {
866 return Vec::new();
867 }
868
869 let pinned_columns = self.dataview.get_pinned_columns();
871 let pinned_count = pinned_columns.len();
872
873 let mut used_width = 0u16;
874 let separator_width = 1u16;
875 let mut result = Vec::new();
876
877 tracing::debug!("[PIN_DEBUG] === calculate_visible_column_indices ===");
878 tracing::debug!(
879 "[PIN_DEBUG] available_width={}, total_visual_columns={}",
880 available_width,
881 total_visual_columns
882 );
883 tracing::debug!(
884 "[PIN_DEBUG] pinned_columns={:?} (count={})",
885 pinned_columns,
886 pinned_count
887 );
888 tracing::debug!("[PIN_DEBUG] viewport_cols={:?}", self.viewport_cols);
889 tracing::debug!("[PIN_DEBUG] display_columns={:?}", display_columns);
890
891 debug!(target: "viewport_manager",
892 "calculate_visible_column_indices: available_width={}, total_visual_columns={}, pinned_count={}, viewport_start={}",
893 available_width, total_visual_columns, pinned_count, self.viewport_cols.start);
894
895 for visual_idx in 0..pinned_count {
897 if visual_idx >= display_columns.len() {
898 break;
899 }
900
901 let datatable_idx = display_columns[visual_idx];
902 let width = self.width_calculator.get_column_width(
903 &self.dataview,
904 &self.viewport_rows,
905 datatable_idx,
906 );
907
908 used_width += width + separator_width;
910 result.push(datatable_idx);
911 tracing::debug!(
912 "[PIN_DEBUG] Added pinned column: visual_idx={}, datatable_idx={}, width={}",
913 visual_idx,
914 datatable_idx,
915 width
916 );
917 }
918
919 let scrollable_start = self.viewport_cols.start;
922 let visual_start = scrollable_start + pinned_count;
923
924 tracing::debug!(
925 "[PIN_DEBUG] viewport_cols.start={} is SCROLLABLE index",
926 self.viewport_cols.start
927 );
928 tracing::debug!(
929 "[PIN_DEBUG] visual_start={} (scrollable_start {} + pinned_count {})",
930 visual_start,
931 scrollable_start,
932 pinned_count
933 );
934
935 let visual_start = visual_start.min(total_visual_columns);
936
937 for visual_idx in visual_start..total_visual_columns {
939 let datatable_idx = display_columns[visual_idx];
941
942 let width = self.width_calculator.get_column_width(
943 &self.dataview,
944 &self.viewport_rows,
945 datatable_idx,
946 );
947
948 if used_width + width + separator_width <= available_width {
949 used_width += width + separator_width;
950 result.push(datatable_idx);
951 tracing::debug!("[PIN_DEBUG] Added scrollable column: visual_idx={}, datatable_idx={}, width={}", visual_idx, datatable_idx, width);
952 } else {
953 tracing::debug!(
954 "[PIN_DEBUG] Stopped at visual_idx={} - would exceed width",
955 visual_idx
956 );
957 break;
958 }
959 }
960
961 if result.is_empty() && total_visual_columns > 0 {
964 result.push(display_columns[0]);
965 }
966
967 tracing::debug!("[PIN_DEBUG] Final result: {:?}", result);
968 tracing::debug!("[PIN_DEBUG] === End calculate_visible_column_indices ===");
969 debug!(target: "viewport_manager",
970 "calculate_visible_column_indices RESULT: pinned={}, viewport_start={}, visual_start={} -> DataTable indices {:?}",
971 pinned_count, self.viewport_cols.start, visual_start, result);
972
973 result
974 }
975
976 pub fn calculate_columns_that_fit(&mut self, start_col: usize, available_width: u16) -> usize {
979 let mut used_width = 0u16;
982 let mut column_count = 0usize;
983 let separator_width = 1u16;
984
985 for col_idx in start_col..self.dataview.column_count() {
986 let width = self.width_calculator.get_column_width(
987 &self.dataview,
988 &self.viewport_rows,
989 col_idx,
990 );
991 if used_width + width + separator_width <= available_width {
992 used_width += width + separator_width;
993 column_count += 1;
994 } else {
995 break;
996 }
997 }
998
999 column_count.max(1) }
1001
1002 pub fn get_column_widths_for(&mut self, column_indices: &[usize]) -> Vec<u16> {
1005 column_indices
1006 .iter()
1007 .map(|&idx| {
1008 self.width_calculator
1009 .get_column_width(&self.dataview, &self.viewport_rows, idx)
1010 })
1011 .collect()
1012 }
1013
1014 pub fn update_column_viewport(&mut self, start_col: usize, available_width: u16) {
1017 let col_count = self.calculate_columns_that_fit(start_col, available_width);
1018 let end_col = (start_col + col_count).min(self.dataview.column_count());
1019
1020 if self.viewport_cols.start != start_col || self.viewport_cols.end != end_col {
1021 self.viewport_cols = start_col..end_col;
1022 self.cache_dirty = true;
1023 }
1024 }
1025
1026 #[must_use]
1028 pub fn dataview(&self) -> &DataView {
1029 &self.dataview
1030 }
1031
1032 #[must_use]
1035 pub fn clone_dataview(&self) -> DataView {
1036 (*self.dataview).clone()
1037 }
1038
1039 pub fn calculate_optimal_offset_for_last_column(&mut self, available_width: u16) -> usize {
1043 let display_columns = self.dataview.get_display_columns();
1047 if display_columns.is_empty() {
1048 return 0;
1049 }
1050
1051 let pinned = self.dataview.get_pinned_columns();
1052 let pinned_count = pinned.len();
1053
1054 let mut pinned_width = 0u16;
1056 let separator_width = 1u16;
1057 for &col_idx in pinned {
1058 let width = self.width_calculator.get_column_width(
1059 &self.dataview,
1060 &self.viewport_rows,
1061 col_idx,
1062 );
1063 pinned_width += width + separator_width;
1064 }
1065
1066 let available_for_scrollable = available_width.saturating_sub(pinned_width);
1068
1069 let scrollable_columns: Vec<usize> = display_columns
1071 .iter()
1072 .filter(|&&col| !pinned.contains(&col))
1073 .copied()
1074 .collect();
1075
1076 if scrollable_columns.is_empty() {
1077 return 0;
1078 }
1079
1080 let last_col_idx = *scrollable_columns.last().unwrap();
1082 let last_col_width = self.width_calculator.get_column_width(
1083 &self.dataview,
1084 &self.viewport_rows,
1085 last_col_idx,
1086 );
1087
1088 tracing::debug!(
1089 "Starting calculation: last_col_idx={}, width={}w, available={}w, scrollable_cols={}",
1090 last_col_idx,
1091 last_col_width,
1092 available_for_scrollable,
1093 scrollable_columns.len()
1094 );
1095
1096 let mut accumulated_width = last_col_width + separator_width;
1097 let mut best_offset = scrollable_columns.len() - 1; for (idx, &col_idx) in scrollable_columns.iter().enumerate().rev().skip(1) {
1101 let width = self.width_calculator.get_column_width(
1102 &self.dataview,
1103 &self.viewport_rows,
1104 col_idx,
1105 );
1106
1107 let width_with_separator = width + separator_width;
1108
1109 if accumulated_width + width_with_separator <= available_for_scrollable {
1110 accumulated_width += width_with_separator;
1112 best_offset = idx; tracing::trace!(
1114 "Column {} (idx {}) fits ({}w), accumulated={}w, new offset={}",
1115 col_idx,
1116 idx,
1117 width,
1118 accumulated_width,
1119 best_offset
1120 );
1121 } else {
1122 best_offset = idx + 1;
1125 tracing::trace!(
1126 "Column {} doesn't fit ({}w would make {}w total), stopping at offset {}",
1127 col_idx,
1128 width,
1129 accumulated_width + width_with_separator,
1130 best_offset
1131 );
1132 break;
1133 }
1134 }
1135
1136 let mut test_width = 0u16;
1142 let mut can_see_last = false;
1143 for idx in best_offset..scrollable_columns.len() {
1144 let col_idx = scrollable_columns[idx];
1145 let width = self.width_calculator.get_column_width(
1146 &self.dataview,
1147 &self.viewport_rows,
1148 col_idx,
1149 );
1150 test_width += width + separator_width;
1151
1152 if test_width > available_for_scrollable {
1153 tracing::warn!(
1156 "Offset {} doesn't show last column! Need {}w but have {}w",
1157 best_offset,
1158 test_width,
1159 available_for_scrollable
1160 );
1161 best_offset += 1;
1163 can_see_last = false;
1164 break;
1165 }
1166 if idx == scrollable_columns.len() - 1 {
1167 can_see_last = true;
1168 }
1169 }
1170
1171 while !can_see_last && best_offset < scrollable_columns.len() {
1173 test_width = 0;
1174 for idx in best_offset..scrollable_columns.len() {
1175 let col_idx = scrollable_columns[idx];
1176 let width = self.width_calculator.get_column_width(
1177 &self.dataview,
1178 &self.viewport_rows,
1179 col_idx,
1180 );
1181 test_width += width + separator_width;
1182
1183 if test_width > available_for_scrollable {
1184 best_offset += 1;
1185 break;
1186 }
1187 if idx == scrollable_columns.len() - 1 {
1188 can_see_last = true;
1189 }
1190 }
1191 }
1192
1193 tracing::debug!(
1195 "Final offset for last column: scrollable_offset={}, fits {} columns, last col width: {}w, verified last col visible: {}",
1196 best_offset,
1197 scrollable_columns.len() - best_offset,
1198 last_col_width,
1199 can_see_last
1200 );
1201
1202 best_offset
1203 }
1204
1205 pub fn debug_dump(&mut self, available_width: u16) -> String {
1207 let mut output = String::new();
1210 output.push_str("========== VIEWPORT MANAGER DEBUG ==========\n");
1211
1212 let total_cols = self.dataview.column_count();
1213 let pinned = self.dataview.get_pinned_columns();
1214 let pinned_count = pinned.len();
1215
1216 output.push_str(&format!("Total columns: {total_cols}\n"));
1217 output.push_str(&format!("Pinned columns: {pinned:?}\n"));
1218 output.push_str(&format!("Available width: {available_width}w\n"));
1219 output.push_str(&format!("Current viewport: {:?}\n", self.viewport_cols));
1220 output.push_str(&format!(
1221 "Packing mode: {} (Alt+S to cycle)\n",
1222 self.width_calculator.get_packing_mode().display_name()
1223 ));
1224 output.push('\n');
1225
1226 output.push_str("=== COLUMN WIDTH CALCULATIONS ===\n");
1228 output.push_str(&format!(
1229 "Mode: {}\n",
1230 self.width_calculator.get_packing_mode().display_name()
1231 ));
1232
1233 let debug_info = self.width_calculator.get_debug_info();
1235 if !debug_info.is_empty() {
1236 output.push_str("Visible columns in viewport:\n");
1237
1238 let mut visible_count = 0;
1240 for col_idx in self.viewport_cols.clone() {
1241 if col_idx < debug_info.len() {
1242 let (ref col_name, header_width, max_data_width, final_width, sample_count) =
1243 debug_info[col_idx];
1244
1245 let reason = match self.width_calculator.get_packing_mode() {
1247 ColumnPackingMode::DataFocus => {
1248 if max_data_width <= 3 {
1249 format!("Ultra aggressive (data:{max_data_width}≤3 chars)")
1250 } else if max_data_width <= 10 && header_width > max_data_width * 2 {
1251 format!(
1252 "Aggressive truncate (data:{}≤10, header:{}>{} )",
1253 max_data_width,
1254 header_width,
1255 max_data_width * 2
1256 )
1257 } else if final_width == MAX_COL_WIDTH_DATA_FOCUS {
1258 "Max width reached".to_string()
1259 } else {
1260 "Data-based width".to_string()
1261 }
1262 }
1263 ColumnPackingMode::HeaderFocus => {
1264 if final_width == header_width + COLUMN_PADDING {
1265 "Full header shown".to_string()
1266 } else if final_width == MAX_COL_WIDTH {
1267 "Max width reached".to_string()
1268 } else {
1269 "Header priority".to_string()
1270 }
1271 }
1272 ColumnPackingMode::Balanced => {
1273 if header_width > max_data_width && final_width < header_width {
1274 "Header constrained by ratio".to_string()
1275 } else {
1276 "Balanced".to_string()
1277 }
1278 }
1279 };
1280
1281 output.push_str(&format!(
1282 " [{col_idx}] \"{col_name}\":\n Header: {header_width}w, Data: {max_data_width}w → Final: {final_width}w ({reason}, {sample_count} samples)\n"
1283 ));
1284
1285 visible_count += 1;
1286
1287 if visible_count >= 10 {
1289 let remaining = self.viewport_cols.end - self.viewport_cols.start - 10;
1290 if remaining > 0 {
1291 output.push_str(&format!(" ... and {remaining} more columns\n"));
1292 }
1293 break;
1294 }
1295 }
1296 }
1297 }
1298
1299 output.push('\n');
1300
1301 output.push_str("Column width summary (all columns):\n");
1303 let all_widths = self
1304 .width_calculator
1305 .get_all_column_widths(&self.dataview, &self.viewport_rows);
1306 for (idx, &width) in all_widths.iter().enumerate() {
1307 if idx >= 20 && idx < total_cols - 10 {
1308 if idx == 20 {
1309 output.push_str(" ... (showing only first 20 and last 10)\n");
1310 }
1311 continue;
1312 }
1313 output.push_str(&format!(" [{idx}] {width}w\n"));
1314 }
1315 output.push('\n');
1316
1317 output.push_str("=== OPTIMAL OFFSET CALCULATION ===\n");
1319 let last_col_idx = total_cols - 1;
1320 let last_col_width = self.width_calculator.get_column_width(
1321 &self.dataview,
1322 &self.viewport_rows,
1323 last_col_idx,
1324 );
1325
1326 let separator_width = 1u16;
1328 let mut pinned_width = 0u16;
1329 for &col_idx in pinned {
1330 let width = self.width_calculator.get_column_width(
1331 &self.dataview,
1332 &self.viewport_rows,
1333 col_idx,
1334 );
1335 pinned_width += width + separator_width;
1336 }
1337 let available_for_scrollable = available_width.saturating_sub(pinned_width);
1338
1339 output.push_str(&format!(
1340 "Last column: {last_col_idx} (width: {last_col_width}w)\n"
1341 ));
1342 output.push_str(&format!("Pinned width: {pinned_width}w\n"));
1343 output.push_str(&format!(
1344 "Available for scrollable: {available_for_scrollable}w\n"
1345 ));
1346 output.push('\n');
1347
1348 let mut accumulated_width = last_col_width + separator_width;
1350 let mut best_offset = last_col_idx;
1351
1352 output.push_str("Backtracking from last column:\n");
1353 output.push_str(&format!(
1354 " Start: column {last_col_idx} = {last_col_width}w (accumulated: {accumulated_width}w)\n"
1355 ));
1356
1357 for col_idx in (pinned_count..last_col_idx).rev() {
1358 let width = self.width_calculator.get_column_width(
1359 &self.dataview,
1360 &self.viewport_rows,
1361 col_idx,
1362 );
1363 let width_with_sep = width + separator_width;
1364
1365 if accumulated_width + width_with_sep <= available_for_scrollable {
1366 accumulated_width += width_with_sep;
1367 best_offset = col_idx;
1368 output.push_str(&format!(
1369 " Column {col_idx} fits: {width}w (accumulated: {accumulated_width}w, offset: {best_offset})\n"
1370 ));
1371 } else {
1372 output.push_str(&format!(
1373 " Column {} doesn't fit: {}w (would make {}w > {}w)\n",
1374 col_idx,
1375 width,
1376 accumulated_width + width_with_sep,
1377 available_for_scrollable
1378 ));
1379 best_offset = col_idx + 1;
1380 break;
1381 }
1382 }
1383
1384 output.push_str(&format!("\nCalculated offset: {best_offset} (absolute)\n"));
1385
1386 output.push_str("\n=== VERIFICATION ===\n");
1388 let mut verify_width = 0u16;
1389 let mut can_show_last = true;
1390
1391 for test_idx in best_offset..=last_col_idx {
1392 let width = self.width_calculator.get_column_width(
1393 &self.dataview,
1394 &self.viewport_rows,
1395 test_idx,
1396 );
1397 verify_width += width + separator_width;
1398
1399 output.push_str(&format!(
1400 " Column {test_idx}: {width}w (running total: {verify_width}w)\n"
1401 ));
1402
1403 if verify_width > available_for_scrollable {
1404 output.push_str(&format!(
1405 " ❌ EXCEEDS LIMIT! {verify_width}w > {available_for_scrollable}w\n"
1406 ));
1407 if test_idx == last_col_idx {
1408 can_show_last = false;
1409 output.push_str(" ❌ LAST COLUMN NOT VISIBLE!\n");
1410 }
1411 break;
1412 }
1413
1414 if test_idx == last_col_idx {
1415 output.push_str(" ✅ LAST COLUMN VISIBLE!\n");
1416 }
1417 }
1418
1419 output.push_str(&format!(
1420 "\nVerification result: last column visible = {can_show_last}\n"
1421 ));
1422
1423 output.push_str("\n=== CURRENT VIEWPORT RESULT ===\n");
1425 let visible_indices = self.calculate_visible_column_indices(available_width);
1426 output.push_str(&format!("Visible columns: {visible_indices:?}\n"));
1427 output.push_str(&format!(
1428 "Last visible column: {}\n",
1429 visible_indices.last().copied().unwrap_or(0)
1430 ));
1431 output.push_str(&format!(
1432 "Shows last column ({}): {}\n",
1433 last_col_idx,
1434 visible_indices.contains(&last_col_idx)
1435 ));
1436
1437 output.push_str("============================================\n");
1438 output
1439 }
1440
1441 #[must_use]
1444 pub fn get_column_names_ordered(&self) -> Vec<String> {
1445 self.dataview.column_names()
1446 }
1447
1448 pub fn get_visible_columns_info(
1451 &mut self,
1452 available_width: u16,
1453 ) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
1454 debug!(target: "viewport_manager",
1455 "get_visible_columns_info CALLED with width={}, current_viewport={:?}",
1456 available_width, self.viewport_cols);
1457
1458 let viewport_indices = self.calculate_visible_column_indices(available_width);
1460
1461 let display_order = self.dataview.get_display_columns();
1463 let mut visible_indices = Vec::new();
1464
1465 for &col_idx in &display_order {
1467 if viewport_indices.contains(&col_idx) {
1468 visible_indices.push(col_idx);
1469 }
1470 }
1471
1472 let pinned_columns = self.dataview.get_pinned_columns();
1474
1475 let mut pinned_visible = Vec::new();
1477 let mut scrollable_visible = Vec::new();
1478
1479 for &idx in &visible_indices {
1480 if pinned_columns.contains(&idx) {
1481 pinned_visible.push(idx);
1482 } else {
1483 scrollable_visible.push(idx);
1484 }
1485 }
1486
1487 debug!(target: "viewport_manager",
1488 "get_visible_columns_info: viewport={:?} -> ordered={:?} ({} pinned, {} scrollable)",
1489 viewport_indices, visible_indices, pinned_visible.len(), scrollable_visible.len());
1490
1491 debug!(target: "viewport_manager",
1492 "RENDERER DEBUG: viewport_indices={:?}, display_order={:?}, visible_indices={:?}",
1493 viewport_indices, display_order, visible_indices);
1494
1495 (visible_indices, pinned_visible, scrollable_visible)
1496 }
1497
1498 pub fn calculate_column_x_positions(&mut self, available_width: u16) -> (Vec<usize>, Vec<u16>) {
1501 let visible_indices = self.calculate_visible_column_indices(available_width);
1502 let mut x_positions = Vec::new();
1503 let mut current_x = 0u16;
1504 let separator_width = 1u16;
1505
1506 for &col_idx in &visible_indices {
1507 x_positions.push(current_x);
1508 let width = self.width_calculator.get_column_width(
1509 &self.dataview,
1510 &self.viewport_rows,
1511 col_idx,
1512 );
1513 current_x += width + separator_width;
1514 }
1515
1516 (visible_indices, x_positions)
1517 }
1518
1519 pub fn get_column_x_position(&mut self, column: usize, available_width: u16) -> Option<u16> {
1521 let (indices, positions) = self.calculate_column_x_positions(available_width);
1522 indices
1523 .iter()
1524 .position(|&idx| idx == column)
1525 .and_then(|pos| positions.get(pos).copied())
1526 }
1527
1528 pub fn calculate_visible_column_indices_ordered(&mut self, available_width: u16) -> Vec<usize> {
1530 let ordered_columns = self.dataview.get_display_columns();
1534 let mut visible_indices = Vec::new();
1535 let mut used_width = 0u16;
1536 let separator_width = 1u16;
1537
1538 tracing::trace!(
1539 "ViewportManager: Starting ordered column layout. Available width: {}w, DataView order: {:?}",
1540 available_width,
1541 ordered_columns
1542 );
1543
1544 for &col_idx in &ordered_columns {
1546 let width = self.width_calculator.get_column_width(
1547 &self.dataview,
1548 &self.viewport_rows,
1549 col_idx,
1550 );
1551
1552 if used_width + width + separator_width <= available_width {
1553 visible_indices.push(col_idx);
1554 used_width += width + separator_width;
1555 tracing::trace!(
1556 "Added column {} in DataView order: {}w (total used: {}w)",
1557 col_idx,
1558 width,
1559 used_width
1560 );
1561 } else {
1562 tracing::trace!(
1563 "Skipped column {} ({}w) - would exceed available width",
1564 col_idx,
1565 width
1566 );
1567 break; }
1569 }
1570
1571 tracing::trace!(
1572 "Final ordered layout: {} columns visible {:?}, {}w used of {}w",
1573 visible_indices.len(),
1574 visible_indices,
1575 used_width,
1576 available_width
1577 );
1578
1579 visible_indices
1580 }
1581
1582 pub fn get_display_position_for_datatable_column(
1585 &mut self,
1586 datatable_column: usize,
1587 available_width: u16,
1588 ) -> Option<usize> {
1589 let visible_columns_info = self.get_visible_columns_info(available_width);
1590 let visible_indices = visible_columns_info.0;
1591
1592 let position = visible_indices
1594 .iter()
1595 .position(|&col| col == datatable_column);
1596
1597 debug!(target: "viewport_manager",
1598 "get_display_position_for_datatable_column: datatable_column={}, visible_indices={:?}, position={:?}",
1599 datatable_column, visible_indices, position);
1600
1601 position
1602 }
1603
1604 pub fn get_crosshair_column(
1609 &mut self,
1610 current_datatable_column: usize,
1611 available_width: u16,
1612 ) -> Option<usize> {
1613 let visible_columns_info = self.get_visible_columns_info(available_width);
1615 let visible_indices = visible_columns_info.0;
1616
1617 let position = visible_indices
1619 .iter()
1620 .position(|&col| col == current_datatable_column);
1621
1622 debug!(target: "viewport_manager",
1623 "CROSSHAIR: current_datatable_column={}, visible_indices={:?}, crosshair_position={:?}",
1624 current_datatable_column, visible_indices, position);
1625
1626 position
1627 }
1628
1629 pub fn get_visual_display(
1633 &mut self,
1634 available_width: u16,
1635 _row_indices: &[usize], ) -> (Vec<String>, Vec<Vec<String>>, Vec<u16>) {
1637 let row_indices: Vec<usize> = (self.viewport_rows.start..self.viewport_rows.end).collect();
1639
1640 debug!(target: "viewport_manager",
1641 "get_visual_display: Using viewport_rows {:?} -> row_indices: {:?} (first 5)",
1642 self.viewport_rows,
1643 row_indices.iter().take(5).collect::<Vec<_>>());
1644 let visible_column_indices = self.calculate_visible_column_indices(available_width);
1647
1648 tracing::debug!(
1649 "[RENDER_DEBUG] visible_column_indices from calculate: {:?}",
1650 visible_column_indices
1651 );
1652
1653 let all_headers = self.dataview.get_display_column_names();
1655 let display_columns = self.dataview.get_display_columns();
1656 let total_visual_columns = all_headers.len();
1657
1658 debug!(target: "viewport_manager",
1659 "get_visual_display: {} total visual columns, viewport: {:?}",
1660 total_visual_columns, self.viewport_cols);
1661
1662 let headers: Vec<String> = visible_column_indices
1664 .iter()
1665 .filter_map(|&dt_idx| {
1666 display_columns
1668 .iter()
1669 .position(|&x| x == dt_idx)
1670 .and_then(|visual_idx| all_headers.get(visual_idx).cloned())
1671 })
1672 .collect();
1673
1674 tracing::debug!("[RENDER_DEBUG] headers: {:?}", headers);
1675
1676 let visual_rows: Vec<Vec<String>> = row_indices
1679 .iter()
1680 .filter_map(|&display_row_idx| {
1681 let row_data = self.dataview.get_row_visual_values(display_row_idx);
1684 if let Some(ref full_row) = row_data {
1685 if !(5..19900).contains(&display_row_idx) {
1687 debug!(target: "viewport_manager",
1688 "DATAVIEW FETCH: display_row_idx {} -> data: {:?} (first 3 cols)",
1689 display_row_idx,
1690 full_row.iter().take(3).collect::<Vec<_>>());
1691 }
1692 }
1693 row_data.map(|full_row| {
1694 visible_column_indices
1696 .iter()
1697 .filter_map(|&dt_idx| {
1698 display_columns
1700 .iter()
1701 .position(|&x| x == dt_idx)
1702 .and_then(|visual_idx| full_row.get(visual_idx).cloned())
1703 })
1704 .collect()
1705 })
1706 })
1707 .collect();
1708
1709 let widths: Vec<u16> = visible_column_indices
1711 .iter()
1712 .map(|&dt_idx| {
1713 self.width_calculator
1714 .get_column_width(&self.dataview, &self.viewport_rows, dt_idx)
1715 })
1716 .collect();
1717
1718 debug!(target: "viewport_manager",
1719 "get_visual_display RESULT: {} headers, {} rows",
1720 headers.len(), visual_rows.len());
1721 if let Some(first_row) = visual_rows.first() {
1722 debug!(target: "viewport_manager",
1723 "Alignment check (FIRST ROW): {:?}",
1724 headers.iter().zip(first_row).take(5)
1725 .map(|(h, v)| format!("{h}: {v}")).collect::<Vec<_>>());
1726 }
1727 if let Some(last_row) = visual_rows.last() {
1728 debug!(target: "viewport_manager",
1729 "Alignment check (LAST ROW): {:?}",
1730 headers.iter().zip(last_row).take(5)
1731 .map(|(h, v)| format!("{h}: {v}")).collect::<Vec<_>>());
1732 }
1733
1734 (headers, visual_rows, widths)
1735 }
1736
1737 pub fn get_visible_column_headers(&self, visible_indices: &[usize]) -> Vec<String> {
1740 let mut headers = Vec::new();
1741
1742 let source = self.dataview.source();
1745 let all_column_names = source.column_names();
1746
1747 for &visual_idx in visible_indices {
1748 if visual_idx < all_column_names.len() {
1749 headers.push(all_column_names[visual_idx].clone());
1750 } else {
1751 headers.push(format!("Column_{visual_idx}"));
1753 }
1754 }
1755
1756 debug!(target: "viewport_manager",
1757 "get_visible_column_headers: indices={:?} -> headers={:?}",
1758 visible_indices, headers);
1759
1760 headers
1761 }
1762
1763 pub fn get_crosshair_column_for_display(
1766 &mut self,
1767 current_display_position: usize,
1768 available_width: u16,
1769 ) -> Option<usize> {
1770 let display_columns = self.dataview.get_display_columns();
1772
1773 if current_display_position >= display_columns.len() {
1775 debug!(target: "viewport_manager",
1776 "CROSSHAIR DISPLAY: display_position {} out of bounds (max {})",
1777 current_display_position, display_columns.len());
1778 return None;
1779 }
1780
1781 let datatable_column = display_columns[current_display_position];
1783
1784 let visible_columns_info = self.get_visible_columns_info(available_width);
1786 let visible_indices = visible_columns_info.0;
1787
1788 let position = visible_indices
1790 .iter()
1791 .position(|&col| col == datatable_column);
1792
1793 debug!(target: "viewport_manager",
1794 "CROSSHAIR DISPLAY: display_pos={} -> datatable_col={} -> visible_indices={:?} -> crosshair_pos={:?}",
1795 current_display_position, datatable_column, visible_indices, position);
1796
1797 position
1798 }
1799
1800 pub fn calculate_efficiency_metrics(&mut self, available_width: u16) -> ViewportEfficiency {
1802 let visible_indices = self.calculate_visible_column_indices(available_width);
1804
1805 let mut used_width = 0u16;
1807 let separator_width = 1u16;
1808
1809 for &col_idx in &visible_indices {
1810 let width = self.width_calculator.get_column_width(
1811 &self.dataview,
1812 &self.viewport_rows,
1813 col_idx,
1814 );
1815 used_width += width + separator_width;
1816 }
1817
1818 if !visible_indices.is_empty() {
1820 used_width = used_width.saturating_sub(separator_width);
1821 }
1822
1823 let wasted_space = available_width.saturating_sub(used_width);
1824
1825 let next_column_width = if visible_indices.is_empty() {
1827 None
1828 } else {
1829 let last_visible = *visible_indices.last().unwrap();
1830 if last_visible + 1 < self.dataview.column_count() {
1831 Some(self.width_calculator.get_column_width(
1832 &self.dataview,
1833 &self.viewport_rows,
1834 last_visible + 1,
1835 ))
1836 } else {
1837 None
1838 }
1839 };
1840
1841 let mut columns_that_could_fit = Vec::new();
1843 if wasted_space > MIN_COL_WIDTH + separator_width {
1844 let all_widths = self
1845 .width_calculator
1846 .get_all_column_widths(&self.dataview, &self.viewport_rows);
1847 for (idx, &width) in all_widths.iter().enumerate() {
1848 if !visible_indices.contains(&idx) && width + separator_width <= wasted_space {
1850 columns_that_could_fit.push((idx, width));
1851 }
1852 }
1853 }
1854
1855 let efficiency_percent = if available_width > 0 {
1856 ((f32::from(used_width) / f32::from(available_width)) * 100.0) as u8
1857 } else {
1858 0
1859 };
1860
1861 ViewportEfficiency {
1862 available_width,
1863 used_width,
1864 wasted_space,
1865 efficiency_percent,
1866 visible_columns: visible_indices.len(),
1867 column_widths: visible_indices
1868 .iter()
1869 .map(|&idx| {
1870 self.width_calculator
1871 .get_column_width(&self.dataview, &self.viewport_rows, idx)
1872 })
1873 .collect(),
1874 next_column_width,
1875 columns_that_could_fit,
1876 }
1877 }
1878
1879 pub fn navigate_to_first_column(&mut self) -> NavigationResult {
1882 if self.viewport_lock {
1884 self.crosshair_col = self.viewport_cols.start;
1886 return NavigationResult {
1887 column_position: self.crosshair_col,
1888 scroll_offset: self.viewport_cols.start,
1889 description: "Moved to first visible column (viewport locked)".to_string(),
1890 viewport_changed: false,
1891 };
1892 }
1893 let pinned_count = self.dataview.get_pinned_columns().len();
1895 let pinned_names = self.dataview.get_pinned_column_names();
1896
1897 let first_scrollable_column = pinned_count;
1899
1900 let new_scroll_offset = 0;
1902 let old_scroll_offset = self.viewport_cols.start;
1903
1904 let visible_indices = self
1906 .calculate_visible_column_indices_with_offset(self.terminal_width, new_scroll_offset);
1907 let viewport_end = if let Some(&last_idx) = visible_indices.last() {
1908 last_idx + 1
1909 } else {
1910 new_scroll_offset + 1
1911 };
1912
1913 self.viewport_cols = new_scroll_offset..viewport_end;
1915
1916 self.crosshair_col = first_scrollable_column;
1918
1919 let description = if pinned_count > 0 {
1921 format!(
1922 "First scrollable column selected (after {pinned_count} pinned: {pinned_names:?})"
1923 )
1924 } else {
1925 "First column selected".to_string()
1926 };
1927
1928 let viewport_changed = old_scroll_offset != new_scroll_offset;
1929
1930 debug!(target: "viewport_manager",
1931 "navigate_to_first_column: pinned={}, first_scrollable={}, crosshair_col={}, scroll_offset={}->{}",
1932 pinned_count, first_scrollable_column, self.crosshair_col, old_scroll_offset, new_scroll_offset);
1933
1934 NavigationResult {
1935 column_position: first_scrollable_column,
1936 scroll_offset: new_scroll_offset,
1937 description,
1938 viewport_changed,
1939 }
1940 }
1941
1942 pub fn navigate_to_last_column(&mut self) -> NavigationResult {
1945 if self.viewport_lock {
1947 self.crosshair_col = self.viewport_cols.end.saturating_sub(1);
1949 return NavigationResult {
1950 column_position: self.crosshair_col,
1951 scroll_offset: self.viewport_cols.start,
1952 description: "Moved to last visible column (viewport locked)".to_string(),
1953 viewport_changed: false,
1954 };
1955 }
1956 let display_columns = self.dataview.get_display_columns();
1958 let total_visual_columns = display_columns.len();
1959
1960 if total_visual_columns == 0 {
1961 return NavigationResult {
1962 column_position: 0,
1963 scroll_offset: 0,
1964 description: "No columns available".to_string(),
1965 viewport_changed: false,
1966 };
1967 }
1968
1969 let last_visual_column = total_visual_columns - 1;
1971
1972 self.crosshair_col = last_visual_column;
1974
1975 let available_width = self.terminal_width;
1978 let pinned_count = self.dataview.get_pinned_columns().len();
1979
1980 let mut pinned_width = 0u16;
1982 for i in 0..pinned_count {
1983 let col_idx = display_columns[i];
1984 let width = self.width_calculator.get_column_width(
1985 &self.dataview,
1986 &self.viewport_rows,
1987 col_idx,
1988 );
1989 pinned_width += width + 3; }
1991
1992 let available_for_scrollable = available_width.saturating_sub(pinned_width);
1993
1994 let mut accumulated_width = 0u16;
1996 let mut new_scroll_offset = last_visual_column;
1997
1998 for visual_idx in (pinned_count..=last_visual_column).rev() {
2000 let col_idx = display_columns[visual_idx];
2001 let width = self.width_calculator.get_column_width(
2002 &self.dataview,
2003 &self.viewport_rows,
2004 col_idx,
2005 );
2006 accumulated_width += width + 3; if accumulated_width > available_for_scrollable {
2009 new_scroll_offset = visual_idx + 1;
2011 break;
2012 }
2013 new_scroll_offset = visual_idx;
2014 }
2015
2016 new_scroll_offset = new_scroll_offset.max(pinned_count);
2018
2019 let old_scroll_offset = self.viewport_cols.start;
2020 let viewport_changed = old_scroll_offset != new_scroll_offset;
2021
2022 let visible_indices = self
2024 .calculate_visible_column_indices_with_offset(self.terminal_width, new_scroll_offset);
2025 let viewport_end = if let Some(&last_idx) = visible_indices.last() {
2026 last_idx + 1
2027 } else {
2028 new_scroll_offset + 1
2029 };
2030
2031 self.viewport_cols = new_scroll_offset..viewport_end;
2033
2034 debug!(target: "viewport_manager",
2035 "navigate_to_last_column: last_visual={}, scroll_offset={}->{}",
2036 last_visual_column, old_scroll_offset, new_scroll_offset);
2037
2038 NavigationResult {
2039 column_position: last_visual_column,
2040 scroll_offset: new_scroll_offset,
2041 description: format!("Last column selected (column {})", last_visual_column + 1),
2042 viewport_changed,
2043 }
2044 }
2045
2046 pub fn navigate_column_left(&mut self, current_display_position: usize) -> NavigationResult {
2050 if self.viewport_lock {
2052 debug!(target: "viewport_manager",
2053 "navigate_column_left: Viewport locked, crosshair_col={}, viewport={:?}",
2054 self.crosshair_col, self.viewport_cols);
2055
2056 if self.crosshair_col > self.viewport_cols.start {
2058 self.crosshair_col -= 1;
2059 return NavigationResult {
2060 column_position: self.crosshair_col,
2061 scroll_offset: self.viewport_cols.start,
2062 description: "Moved within locked viewport".to_string(),
2063 viewport_changed: false,
2064 };
2065 }
2066 return NavigationResult {
2068 column_position: self.crosshair_col,
2069 scroll_offset: self.viewport_cols.start,
2070 description: "At left edge of locked viewport".to_string(),
2071 viewport_changed: false,
2072 };
2073 }
2074
2075 let display_columns = self.dataview.get_display_columns();
2077 let total_display_columns = display_columns.len();
2078
2079 debug!(target: "viewport_manager",
2080 "navigate_column_left: current_display_pos={}, total_display={}, display_order={:?}",
2081 current_display_position, total_display_columns, display_columns);
2082
2083 let current_display_index = if current_display_position < total_display_columns {
2085 current_display_position
2086 } else {
2087 0 };
2089
2090 debug!(target: "viewport_manager",
2091 "navigate_column_left: using display_index={}",
2092 current_display_index);
2093
2094 if current_display_index == 0 {
2097 return NavigationResult {
2100 column_position: 0, scroll_offset: self.viewport_cols.start,
2102 description: "Already at first column".to_string(),
2103 viewport_changed: false,
2104 };
2105 }
2106
2107 let new_display_index = current_display_index - 1;
2108
2109 let new_visual_column = display_columns
2111 .get(new_display_index)
2112 .copied()
2113 .unwrap_or_else(|| {
2114 display_columns
2115 .get(current_display_index)
2116 .copied()
2117 .unwrap_or(0)
2118 });
2119
2120 let old_scroll_offset = self.viewport_cols.start;
2121
2122 debug!(target: "viewport_manager",
2126 "navigate_column_left: moving to datatable_column={}, current viewport={:?}",
2127 new_visual_column, self.viewport_cols);
2128
2129 let viewport_changed = self.set_current_column(new_display_index);
2131
2132 let column_names = self.dataview.column_names();
2135 let column_name = display_columns
2136 .get(new_display_index)
2137 .and_then(|&dt_idx| column_names.get(dt_idx))
2138 .map_or("unknown", std::string::String::as_str);
2139 let description = format!(
2140 "Navigate left to column '{}' ({})",
2141 column_name,
2142 new_display_index + 1
2143 );
2144
2145 debug!(target: "viewport_manager",
2146 "navigate_column_left: display_pos {}→{}, datatable_col: {}, scroll: {}→{}, viewport_changed={}",
2147 current_display_index, new_display_index, new_visual_column,
2148 old_scroll_offset, self.viewport_cols.start, viewport_changed);
2149
2150 NavigationResult {
2151 column_position: new_display_index, scroll_offset: self.viewport_cols.start,
2153 description,
2154 viewport_changed,
2155 }
2156 }
2157
2158 pub fn navigate_column_right(&mut self, current_display_position: usize) -> NavigationResult {
2161 debug!(target: "viewport_manager",
2162 "=== CRITICAL DEBUG: navigate_column_right CALLED ===");
2163 debug!(target: "viewport_manager",
2164 "Input current_display_position: {}", current_display_position);
2165 debug!(target: "viewport_manager",
2166 "Current crosshair_col: {}", self.crosshair_col);
2167 debug!(target: "viewport_manager",
2168 "Current viewport_cols: {:?}", self.viewport_cols);
2169 if self.viewport_lock {
2171 debug!(target: "viewport_manager",
2172 "navigate_column_right: Viewport locked, crosshair_col={}, viewport={:?}",
2173 self.crosshair_col, self.viewport_cols);
2174
2175 if self.crosshair_col < self.viewport_cols.end - 1 {
2177 self.crosshair_col += 1;
2178 return NavigationResult {
2179 column_position: self.crosshair_col,
2180 scroll_offset: self.viewport_cols.start,
2181 description: "Moved within locked viewport".to_string(),
2182 viewport_changed: false,
2183 };
2184 }
2185 return NavigationResult {
2187 column_position: self.crosshair_col,
2188 scroll_offset: self.viewport_cols.start,
2189 description: "At right edge of locked viewport".to_string(),
2190 viewport_changed: false,
2191 };
2192 }
2193
2194 let display_columns = self.dataview.get_display_columns();
2195 let total_display_columns = display_columns.len();
2196 let column_names = self.dataview.column_names();
2197
2198 debug!(target: "viewport_manager",
2200 "=== navigate_column_right DETAILED DEBUG ===");
2201 debug!(target: "viewport_manager",
2202 "ENTRY: current_display_pos={}, total_display_columns={}",
2203 current_display_position, total_display_columns);
2204 debug!(target: "viewport_manager",
2205 "display_columns (DataTable indices): {:?}", display_columns);
2206
2207 if current_display_position < display_columns.len() {
2209 let current_dt_idx = display_columns[current_display_position];
2210 let current_name = column_names
2211 .get(current_dt_idx)
2212 .map_or("unknown", std::string::String::as_str);
2213 debug!(target: "viewport_manager",
2214 "Current position {} -> column '{}' (dt_idx={})",
2215 current_display_position, current_name, current_dt_idx);
2216 }
2217
2218 if current_display_position + 1 < display_columns.len() {
2219 let next_dt_idx = display_columns[current_display_position + 1];
2220 let next_name = column_names
2221 .get(next_dt_idx)
2222 .map_or("unknown", std::string::String::as_str);
2223 debug!(target: "viewport_manager",
2224 "Next position {} -> column '{}' (dt_idx={})",
2225 current_display_position + 1, next_name, next_dt_idx);
2226 }
2227
2228 let current_display_index = if current_display_position < total_display_columns {
2230 current_display_position
2231 } else {
2232 debug!(target: "viewport_manager",
2233 "WARNING: current_display_position {} >= total_display_columns {}, resetting to 0",
2234 current_display_position, total_display_columns);
2235 0 };
2237
2238 debug!(target: "viewport_manager",
2239 "Validated: current_display_index={}",
2240 current_display_index);
2241
2242 if current_display_index + 1 >= total_display_columns {
2245 let last_display_index = total_display_columns.saturating_sub(1);
2247 debug!(target: "viewport_manager",
2248 "At last column boundary: current={}, total={}, returning last_display_index={}",
2249 current_display_index, total_display_columns, last_display_index);
2250 return NavigationResult {
2251 column_position: last_display_index, scroll_offset: self.viewport_cols.start,
2253 description: "Already at last column".to_string(),
2254 viewport_changed: false,
2255 };
2256 }
2257
2258 let new_display_index = current_display_index + 1;
2259
2260 let new_visual_column = display_columns
2262 .get(new_display_index)
2263 .copied()
2264 .unwrap_or_else(|| {
2265 tracing::error!(
2267 "[NAV_ERROR] Failed to get display column at index {}, total={}",
2268 new_display_index,
2269 display_columns.len()
2270 );
2271 display_columns
2273 .get(current_display_index)
2274 .copied()
2275 .unwrap_or(0)
2276 });
2277
2278 debug!(target: "viewport_manager",
2279 "navigate_column_right: display_pos {}→{}, new_visual_column={}",
2280 current_display_index, new_display_index, new_visual_column);
2281
2282 let old_scroll_offset = self.viewport_cols.start;
2283
2284 debug!(target: "viewport_manager",
2290 "navigate_column_right: moving to datatable_column={}, current viewport={:?}",
2291 new_visual_column, self.viewport_cols);
2292
2293 debug!(target: "viewport_manager",
2296 "navigate_column_right: before set_current_column(visual_idx={}), viewport={:?}",
2297 new_display_index, self.viewport_cols);
2298 let viewport_changed = self.set_current_column(new_display_index);
2299 debug!(target: "viewport_manager",
2300 "navigate_column_right: after set_current_column(visual_idx={}), viewport={:?}, changed={}",
2301 new_display_index, self.viewport_cols, viewport_changed);
2302
2303 let column_names = self.dataview.column_names();
2306 let column_name = display_columns
2307 .get(new_display_index)
2308 .and_then(|&dt_idx| column_names.get(dt_idx))
2309 .map_or("unknown", std::string::String::as_str);
2310 let description = format!(
2311 "Navigate right to column '{}' ({})",
2312 column_name,
2313 new_display_index + 1
2314 );
2315
2316 debug!(target: "viewport_manager",
2318 "=== navigate_column_right RESULT ===");
2319 debug!(target: "viewport_manager",
2320 "Returning: column_position={} (visual/display index)", new_display_index);
2321 debug!(target: "viewport_manager",
2322 "Movement: {} -> {} (visual indices)", current_display_index, new_display_index);
2323 debug!(target: "viewport_manager",
2324 "Viewport: {:?}, changed={}", self.viewport_cols, viewport_changed);
2325 debug!(target: "viewport_manager",
2326 "Description: {}", description);
2327
2328 tracing::debug!("[NAV_DEBUG] Final result: column_position={} (visual/display idx), viewport_changed={}",
2329 new_display_index, viewport_changed);
2330 debug!(target: "viewport_manager",
2331 "navigate_column_right EXIT: display_pos {}→{}, datatable_col: {}, viewport: {:?}, scroll: {}→{}, viewport_changed={}",
2332 current_display_index, new_display_index, new_visual_column,
2333 self.viewport_cols, old_scroll_offset, self.viewport_cols.start, viewport_changed);
2334
2335 NavigationResult {
2336 column_position: new_display_index, scroll_offset: self.viewport_cols.start,
2338 description,
2339 viewport_changed,
2340 }
2341 }
2342
2343 pub fn page_down(&mut self) -> RowNavigationResult {
2345 let total_rows = self.dataview.row_count();
2346 let visible_rows = self.terminal_height.saturating_sub(6) as usize; debug!(target: "viewport_manager",
2350 "page_down: crosshair_row={}, total_rows={}, visible_rows={}, current_viewport_rows={:?}",
2351 self.crosshair_row, total_rows, visible_rows, self.viewport_rows);
2352
2353 if self.viewport_lock {
2355 debug!(target: "viewport_manager",
2356 "page_down: Viewport locked, moving within current viewport");
2357 let new_row = self
2359 .viewport_rows
2360 .end
2361 .saturating_sub(1)
2362 .min(total_rows.saturating_sub(1));
2363 self.crosshair_row = new_row;
2364 return RowNavigationResult {
2365 row_position: new_row,
2366 row_scroll_offset: self.viewport_rows.start,
2367 description: format!(
2368 "Page down within locked viewport: row {} → {}",
2369 self.crosshair_row + 1,
2370 new_row + 1
2371 ),
2372 viewport_changed: false,
2373 };
2374 }
2375
2376 if self.cursor_lock {
2378 if let Some(lock_position) = self.cursor_lock_position {
2379 debug!(target: "viewport_manager",
2380 "page_down: Cursor locked at position {}", lock_position);
2381
2382 let old_scroll_offset = self.viewport_rows.start;
2384 let max_scroll = total_rows.saturating_sub(visible_rows);
2385 let new_scroll_offset = (old_scroll_offset + visible_rows).min(max_scroll);
2386
2387 if new_scroll_offset == old_scroll_offset {
2388 return RowNavigationResult {
2390 row_position: self.crosshair_row,
2391 row_scroll_offset: old_scroll_offset,
2392 description: "Already at bottom".to_string(),
2393 viewport_changed: false,
2394 };
2395 }
2396
2397 self.viewport_rows =
2399 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2400
2401 self.crosshair_row =
2403 (new_scroll_offset + lock_position).min(total_rows.saturating_sub(1));
2404
2405 return RowNavigationResult {
2406 row_position: self.crosshair_row,
2407 row_scroll_offset: new_scroll_offset,
2408 description: format!(
2409 "Page down with cursor lock (viewport {} → {})",
2410 old_scroll_offset + 1,
2411 new_scroll_offset + 1
2412 ),
2413 viewport_changed: true,
2414 };
2415 }
2416 }
2417
2418 let new_row = (self.crosshair_row + visible_rows).min(total_rows.saturating_sub(1));
2421 self.crosshair_row = new_row;
2422
2423 let old_scroll_offset = self.viewport_rows.start;
2425 let new_scroll_offset = if new_row >= self.viewport_rows.start + visible_rows {
2426 (new_row + 1).saturating_sub(visible_rows)
2428 } else {
2429 old_scroll_offset
2431 };
2432
2433 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2435 let viewport_changed = new_scroll_offset != old_scroll_offset;
2436
2437 let description = format!(
2438 "Page down: row {} → {} (of {})",
2439 self.crosshair_row + 1,
2440 new_row + 1,
2441 total_rows
2442 );
2443
2444 debug!(target: "viewport_manager",
2445 "page_down result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2446 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2447
2448 RowNavigationResult {
2449 row_position: new_row,
2450 row_scroll_offset: new_scroll_offset,
2451 description,
2452 viewport_changed,
2453 }
2454 }
2455
2456 pub fn page_up(&mut self) -> RowNavigationResult {
2458 let total_rows = self.dataview.row_count();
2459 let visible_rows = self.terminal_height.saturating_sub(6) as usize; debug!(target: "viewport_manager",
2463 "page_up: crosshair_row={}, visible_rows={}, current_viewport_rows={:?}",
2464 self.crosshair_row, visible_rows, self.viewport_rows);
2465
2466 if self.viewport_lock {
2468 debug!(target: "viewport_manager",
2469 "page_up: Viewport locked, moving within current viewport");
2470 let new_row = self.viewport_rows.start;
2472 self.crosshair_row = new_row;
2473 return RowNavigationResult {
2474 row_position: new_row,
2475 row_scroll_offset: self.viewport_rows.start,
2476 description: format!(
2477 "Page up within locked viewport: row {} → {}",
2478 self.crosshair_row + 1,
2479 new_row + 1
2480 ),
2481 viewport_changed: false,
2482 };
2483 }
2484
2485 if self.cursor_lock {
2487 if let Some(lock_position) = self.cursor_lock_position {
2488 debug!(target: "viewport_manager",
2489 "page_up: Cursor locked at position {}", lock_position);
2490
2491 let old_scroll_offset = self.viewport_rows.start;
2493 let new_scroll_offset = old_scroll_offset.saturating_sub(visible_rows);
2494
2495 if new_scroll_offset == old_scroll_offset {
2496 return RowNavigationResult {
2498 row_position: self.crosshair_row,
2499 row_scroll_offset: old_scroll_offset,
2500 description: "Already at top".to_string(),
2501 viewport_changed: false,
2502 };
2503 }
2504
2505 self.viewport_rows =
2507 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2508
2509 self.crosshair_row = new_scroll_offset + lock_position;
2511
2512 return RowNavigationResult {
2513 row_position: self.crosshair_row,
2514 row_scroll_offset: new_scroll_offset,
2515 description: format!(
2516 "Page up with cursor lock (viewport {} → {})",
2517 old_scroll_offset + 1,
2518 new_scroll_offset + 1
2519 ),
2520 viewport_changed: true,
2521 };
2522 }
2523 }
2524
2525 let new_row = self.crosshair_row.saturating_sub(visible_rows);
2528 self.crosshair_row = new_row;
2529
2530 let old_scroll_offset = self.viewport_rows.start;
2532 let new_scroll_offset = if new_row < self.viewport_rows.start {
2533 new_row
2535 } else {
2536 old_scroll_offset
2538 };
2539
2540 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2542 let viewport_changed = new_scroll_offset != old_scroll_offset;
2543
2544 let description = format!("Page up: row {} → {}", self.crosshair_row + 1, new_row + 1);
2545
2546 debug!(target: "viewport_manager",
2547 "page_up result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2548 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2549
2550 RowNavigationResult {
2551 row_position: new_row,
2552 row_scroll_offset: new_scroll_offset,
2553 description,
2554 viewport_changed,
2555 }
2556 }
2557
2558 pub fn half_page_down(&mut self) -> RowNavigationResult {
2560 let total_rows = self.dataview.row_count();
2561 let visible_rows = self.terminal_height.saturating_sub(6) as usize; let half_page = visible_rows / 2;
2564
2565 debug!(target: "viewport_manager",
2566 "half_page_down: crosshair_row={}, total_rows={}, half_page={}, current_viewport_rows={:?}",
2567 self.crosshair_row, total_rows, half_page, self.viewport_rows);
2568
2569 if self.viewport_lock {
2571 debug!(target: "viewport_manager",
2572 "half_page_down: Viewport locked, moving within current viewport");
2573 let new_row = self
2575 .viewport_rows
2576 .end
2577 .saturating_sub(1)
2578 .min(total_rows.saturating_sub(1));
2579 self.crosshair_row = new_row;
2580 return RowNavigationResult {
2581 row_position: new_row,
2582 row_scroll_offset: self.viewport_rows.start,
2583 description: format!(
2584 "Half page down within locked viewport: row {} → {}",
2585 self.crosshair_row + 1,
2586 new_row + 1
2587 ),
2588 viewport_changed: false,
2589 };
2590 }
2591
2592 if self.cursor_lock {
2594 if let Some(lock_position) = self.cursor_lock_position {
2595 debug!(target: "viewport_manager",
2596 "half_page_down: Cursor locked at position {}", lock_position);
2597
2598 let old_scroll_offset = self.viewport_rows.start;
2600 let max_scroll = total_rows.saturating_sub(visible_rows);
2601 let new_scroll_offset = (old_scroll_offset + half_page).min(max_scroll);
2602
2603 if new_scroll_offset == old_scroll_offset {
2604 return RowNavigationResult {
2606 row_position: self.crosshair_row,
2607 row_scroll_offset: old_scroll_offset,
2608 description: "Already at bottom".to_string(),
2609 viewport_changed: false,
2610 };
2611 }
2612
2613 self.viewport_rows =
2615 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2616
2617 self.crosshair_row =
2619 (new_scroll_offset + lock_position).min(total_rows.saturating_sub(1));
2620
2621 return RowNavigationResult {
2622 row_position: self.crosshair_row,
2623 row_scroll_offset: new_scroll_offset,
2624 description: format!(
2625 "Half page down with cursor lock (viewport {} → {})",
2626 old_scroll_offset + 1,
2627 new_scroll_offset + 1
2628 ),
2629 viewport_changed: true,
2630 };
2631 }
2632 }
2633
2634 let new_row = (self.crosshair_row + half_page).min(total_rows.saturating_sub(1));
2637 self.crosshair_row = new_row;
2638
2639 let old_scroll_offset = self.viewport_rows.start;
2641 let new_scroll_offset = if new_row >= self.viewport_rows.start + visible_rows {
2642 (new_row + 1).saturating_sub(visible_rows)
2644 } else {
2645 old_scroll_offset
2647 };
2648
2649 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2651 let viewport_changed = new_scroll_offset != old_scroll_offset;
2652
2653 let description = format!(
2654 "Half page down: row {} → {} (of {})",
2655 self.crosshair_row + 1 - half_page.min(self.crosshair_row),
2656 new_row + 1,
2657 total_rows
2658 );
2659
2660 debug!(target: "viewport_manager",
2661 "half_page_down result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2662 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2663
2664 RowNavigationResult {
2665 row_position: new_row,
2666 row_scroll_offset: new_scroll_offset,
2667 description,
2668 viewport_changed,
2669 }
2670 }
2671
2672 pub fn half_page_up(&mut self) -> RowNavigationResult {
2674 let total_rows = self.dataview.row_count();
2675 let visible_rows = self.terminal_height.saturating_sub(6) as usize; let half_page = visible_rows / 2;
2678
2679 debug!(target: "viewport_manager",
2680 "half_page_up: crosshair_row={}, half_page={}, current_viewport_rows={:?}",
2681 self.crosshair_row, half_page, self.viewport_rows);
2682
2683 if self.viewport_lock {
2685 debug!(target: "viewport_manager",
2686 "half_page_up: Viewport locked, moving within current viewport");
2687 let new_row = self.viewport_rows.start;
2689 self.crosshair_row = new_row;
2690 return RowNavigationResult {
2691 row_position: new_row,
2692 row_scroll_offset: self.viewport_rows.start,
2693 description: format!(
2694 "Half page up within locked viewport: row {} → {}",
2695 self.crosshair_row + 1,
2696 new_row + 1
2697 ),
2698 viewport_changed: false,
2699 };
2700 }
2701
2702 if self.cursor_lock {
2704 if let Some(lock_position) = self.cursor_lock_position {
2705 debug!(target: "viewport_manager",
2706 "half_page_up: Cursor locked at position {}", lock_position);
2707
2708 let old_scroll_offset = self.viewport_rows.start;
2710 let new_scroll_offset = old_scroll_offset.saturating_sub(half_page);
2711
2712 if new_scroll_offset == old_scroll_offset {
2713 return RowNavigationResult {
2715 row_position: self.crosshair_row,
2716 row_scroll_offset: old_scroll_offset,
2717 description: "Already at top".to_string(),
2718 viewport_changed: false,
2719 };
2720 }
2721
2722 self.viewport_rows =
2724 new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2725
2726 self.crosshair_row = new_scroll_offset + lock_position;
2728
2729 return RowNavigationResult {
2730 row_position: self.crosshair_row,
2731 row_scroll_offset: new_scroll_offset,
2732 description: format!(
2733 "Half page up with cursor lock (viewport {} → {})",
2734 old_scroll_offset + 1,
2735 new_scroll_offset + 1
2736 ),
2737 viewport_changed: true,
2738 };
2739 }
2740 }
2741
2742 let new_row = self.crosshair_row.saturating_sub(half_page);
2745 self.crosshair_row = new_row;
2746
2747 let old_scroll_offset = self.viewport_rows.start;
2749 let new_scroll_offset = if new_row < self.viewport_rows.start {
2750 new_row
2752 } else {
2753 old_scroll_offset
2755 };
2756
2757 self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2759 let viewport_changed = new_scroll_offset != old_scroll_offset;
2760
2761 let description = format!(
2762 "Half page up: row {} → {}",
2763 self.crosshair_row + half_page + 1,
2764 new_row + 1
2765 );
2766
2767 debug!(target: "viewport_manager",
2768 "half_page_up result: new_row={}, scroll_offset={}→{}, viewport_changed={}",
2769 new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2770
2771 RowNavigationResult {
2772 row_position: new_row,
2773 row_scroll_offset: new_scroll_offset,
2774 description,
2775 viewport_changed,
2776 }
2777 }
2778
2779 pub fn navigate_to_last_row(&mut self, total_rows: usize) -> RowNavigationResult {
2781 if self.viewport_lock {
2783 let last_visible = self
2785 .viewport_rows
2786 .end
2787 .saturating_sub(1)
2788 .min(total_rows.saturating_sub(1));
2789 self.crosshair_row = last_visible;
2790 return RowNavigationResult {
2791 row_position: self.crosshair_row,
2792 row_scroll_offset: self.viewport_rows.start,
2793 description: "Moved to last visible row (viewport locked)".to_string(),
2794 viewport_changed: false,
2795 };
2796 }
2797 if total_rows == 0 {
2798 return RowNavigationResult {
2799 row_position: 0,
2800 row_scroll_offset: 0,
2801 description: "No rows to navigate".to_string(),
2802 viewport_changed: false,
2803 };
2804 }
2805
2806 let visible_rows = (self.terminal_height as usize).max(10);
2809
2810 let last_row = total_rows - 1;
2812
2813 let new_scroll_offset = total_rows.saturating_sub(visible_rows);
2817
2818 debug!(target: "viewport_manager",
2819 "navigate_to_last_row: total_rows={}, last_row={}, visible_rows={}, new_scroll_offset={}",
2820 total_rows, last_row, visible_rows, new_scroll_offset);
2821
2822 let old_scroll_offset = self.viewport_rows.start;
2824 let viewport_changed = new_scroll_offset != old_scroll_offset;
2825
2826 self.viewport_rows = new_scroll_offset..total_rows.min(new_scroll_offset + visible_rows);
2828
2829 self.crosshair_row = last_row;
2832
2833 let description = format!("Jumped to last row ({}/{})", last_row + 1, total_rows);
2834
2835 debug!(target: "viewport_manager",
2836 "navigate_to_last_row result: row={}, crosshair_row={}, scroll_offset={}→{}, viewport_changed={}",
2837 last_row, self.crosshair_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2838
2839 RowNavigationResult {
2840 row_position: last_row,
2841 row_scroll_offset: new_scroll_offset,
2842 description,
2843 viewport_changed,
2844 }
2845 }
2846
2847 pub fn navigate_to_first_row(&mut self, total_rows: usize) -> RowNavigationResult {
2849 if self.viewport_lock {
2851 self.crosshair_row = self.viewport_rows.start;
2853 return RowNavigationResult {
2854 row_position: self.crosshair_row,
2855 row_scroll_offset: self.viewport_rows.start,
2856 description: "Moved to first visible row (viewport locked)".to_string(),
2857 viewport_changed: false,
2858 };
2859 }
2860 if total_rows == 0 {
2861 return RowNavigationResult {
2862 row_position: 0,
2863 row_scroll_offset: 0,
2864 description: "No rows to navigate".to_string(),
2865 viewport_changed: false,
2866 };
2867 }
2868
2869 let visible_rows = (self.terminal_height as usize).max(10);
2872
2873 let first_row = 0;
2875
2876 let new_scroll_offset = 0;
2878
2879 debug!(target: "viewport_manager",
2880 "navigate_to_first_row: total_rows={}, visible_rows={}",
2881 total_rows, visible_rows);
2882
2883 let old_scroll_offset = self.viewport_rows.start;
2885 let viewport_changed = new_scroll_offset != old_scroll_offset;
2886
2887 self.viewport_rows = 0..visible_rows.min(total_rows);
2889
2890 self.crosshair_row = first_row;
2892
2893 let description = format!("Jumped to first row (1/{total_rows})");
2894
2895 debug!(target: "viewport_manager",
2896 "navigate_to_first_row result: row=0, crosshair_row={}, scroll_offset={}→0, viewport_changed={}",
2897 self.crosshair_row, old_scroll_offset, viewport_changed);
2898
2899 RowNavigationResult {
2900 row_position: first_row,
2901 row_scroll_offset: new_scroll_offset,
2902 description,
2903 viewport_changed,
2904 }
2905 }
2906
2907 pub fn navigate_to_viewport_top(&mut self) -> RowNavigationResult {
2909 let top_row = self.viewport_rows.start;
2910 let old_row = self.crosshair_row;
2911
2912 self.crosshair_row = top_row;
2914
2915 let description = format!("Moved to viewport top (row {})", top_row + 1);
2916
2917 debug!(target: "viewport_manager",
2918 "navigate_to_viewport_top: crosshair {} -> {}",
2919 old_row, self.crosshair_row);
2920
2921 RowNavigationResult {
2922 row_position: self.crosshair_row,
2923 row_scroll_offset: self.viewport_rows.start,
2924 description,
2925 viewport_changed: false, }
2927 }
2928
2929 pub fn navigate_to_viewport_middle(&mut self) -> RowNavigationResult {
2931 let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
2933 let middle_offset = viewport_height / 2;
2934 let middle_row = self.viewport_rows.start + middle_offset;
2935 let old_row = self.crosshair_row;
2936
2937 self.crosshair_row = middle_row;
2939
2940 let description = format!("Moved to viewport middle (row {})", middle_row + 1);
2941
2942 debug!(target: "viewport_manager",
2943 "navigate_to_viewport_middle: crosshair {} -> {}",
2944 old_row, self.crosshair_row);
2945
2946 RowNavigationResult {
2947 row_position: self.crosshair_row,
2948 row_scroll_offset: self.viewport_rows.start,
2949 description,
2950 viewport_changed: false, }
2952 }
2953
2954 pub fn navigate_to_viewport_bottom(&mut self) -> RowNavigationResult {
2956 let bottom_row = self.viewport_rows.end.saturating_sub(1);
2959 let old_row = self.crosshair_row;
2960
2961 self.crosshair_row = bottom_row;
2963
2964 let description = format!("Moved to viewport bottom (row {})", bottom_row + 1);
2965
2966 debug!(target: "viewport_manager",
2967 "navigate_to_viewport_bottom: crosshair {} -> {}",
2968 old_row, self.crosshair_row);
2969
2970 RowNavigationResult {
2971 row_position: self.crosshair_row,
2972 row_scroll_offset: self.viewport_rows.start,
2973 description,
2974 viewport_changed: false, }
2976 }
2977
2978 pub fn toggle_cursor_lock(&mut self) -> (bool, String) {
2981 self.cursor_lock = !self.cursor_lock;
2982
2983 if self.cursor_lock {
2984 let relative_position = self.crosshair_row.saturating_sub(self.viewport_rows.start);
2986 self.cursor_lock_position = Some(relative_position);
2987
2988 let description = format!(
2989 "Cursor lock: ON (locked at viewport position {})",
2990 relative_position + 1
2991 );
2992 debug!(target: "viewport_manager",
2993 "Cursor lock enabled: crosshair at viewport position {}",
2994 relative_position);
2995 (true, description)
2996 } else {
2997 self.cursor_lock_position = None;
2998 let description = "Cursor lock: OFF".to_string();
2999 debug!(target: "viewport_manager", "Cursor lock disabled");
3000 (false, description)
3001 }
3002 }
3003
3004 pub fn toggle_viewport_lock(&mut self) -> (bool, String) {
3006 self.viewport_lock = !self.viewport_lock;
3007
3008 if self.viewport_lock {
3009 self.viewport_lock_boundaries = Some(self.viewport_rows.clone());
3011
3012 let description = format!(
3013 "Viewport lock: ON (no scrolling, cursor constrained to rows {}-{})",
3014 self.viewport_rows.start + 1,
3015 self.viewport_rows.end
3016 );
3017 debug!(target: "viewport_manager",
3018 "VIEWPORT LOCK ENABLED: boundaries {:?}, crosshair={}, viewport={:?}",
3019 self.viewport_lock_boundaries, self.crosshair_row, self.viewport_rows);
3020 (true, description)
3021 } else {
3022 self.viewport_lock_boundaries = None;
3023 let description = "Viewport lock: OFF (normal scrolling)".to_string();
3024 debug!(target: "viewport_manager", "VIEWPORT LOCK DISABLED");
3025 (false, description)
3026 }
3027 }
3028
3029 #[must_use]
3031 pub fn is_cursor_locked(&self) -> bool {
3032 self.cursor_lock
3033 }
3034
3035 #[must_use]
3037 pub fn is_viewport_locked(&self) -> bool {
3038 self.viewport_lock
3039 }
3040
3041 pub fn lock_viewport(&mut self) {
3043 if !self.viewport_lock {
3044 self.viewport_lock = true;
3045 self.viewport_lock_boundaries = Some(self.viewport_rows.clone());
3046 debug!(target: "viewport_manager", "Viewport locked: rows {}-{}",
3047 self.viewport_rows.start + 1, self.viewport_rows.end);
3048 }
3049 }
3050
3051 pub fn unlock_viewport(&mut self) {
3053 if self.viewport_lock {
3054 self.viewport_lock = false;
3055 self.viewport_lock_boundaries = None;
3056 debug!(target: "viewport_manager", "Viewport unlocked");
3057 }
3058 }
3059
3060 pub fn reorder_column_left(&mut self, current_column: usize) -> ColumnReorderResult {
3062 debug!(target: "viewport_manager",
3063 "reorder_column_left: current_column={}, viewport={:?}",
3064 current_column, self.viewport_cols
3065 );
3066
3067 let column_count = self.dataview.column_count();
3069
3070 if current_column >= column_count {
3071 return ColumnReorderResult {
3072 new_column_position: current_column,
3073 description: "Invalid column position".to_string(),
3074 success: false,
3075 };
3076 }
3077
3078 let pinned_count = self.dataview.get_pinned_columns().len();
3080
3081 debug!(target: "viewport_manager",
3082 "Before move: column_count={}, pinned_count={}, current_column={}",
3083 column_count, pinned_count, current_column
3084 );
3085
3086 let mut new_dataview = (*self.dataview).clone();
3088
3089 let success = new_dataview.move_column_left(current_column);
3091
3092 if success {
3093 self.dataview = Arc::new(new_dataview);
3095 }
3096
3097 if success {
3098 self.invalidate_cache(); let wrapped_to_end =
3102 current_column == 0 || (current_column == pinned_count && pinned_count > 0);
3103 let new_position = if wrapped_to_end {
3104 column_count - 1
3106 } else {
3107 current_column - 1
3109 };
3110
3111 let column_names = self.dataview.column_names();
3112 let column_name = column_names
3113 .get(new_position)
3114 .map_or("?", std::string::String::as_str);
3115
3116 debug!(target: "viewport_manager",
3117 "After move: new_position={}, wrapped_to_end={}, column_name={}",
3118 new_position, wrapped_to_end, column_name
3119 );
3120
3121 if wrapped_to_end {
3123 let optimal_offset = self.calculate_optimal_offset_for_last_column(
3125 self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH),
3126 );
3127 debug!(target: "viewport_manager",
3128 "Column wrapped to end! Adjusting viewport from {:?} to {}..{}",
3129 self.viewport_cols, optimal_offset, self.dataview.column_count()
3130 );
3131 self.viewport_cols = optimal_offset..self.dataview.column_count();
3132 } else {
3133 if !self.viewport_cols.contains(&new_position) {
3135 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3137
3138 let columns_that_fit =
3140 self.calculate_columns_that_fit(new_position, terminal_width);
3141
3142 let new_start = if new_position < self.viewport_cols.start {
3144 new_position
3146 } else {
3147 new_position.saturating_sub(columns_that_fit - 1)
3149 };
3150
3151 let new_end = (new_start + columns_that_fit).min(self.dataview.column_count());
3152 self.viewport_cols = new_start..new_end;
3153
3154 debug!(target: "viewport_manager",
3155 "Column moved outside viewport! Adjusting viewport to {}..{} to show column {} at position {}",
3156 new_start, new_end, column_name, new_position
3157 );
3158 }
3159 }
3160
3161 self.crosshair_col = new_position;
3163
3164 ColumnReorderResult {
3165 new_column_position: new_position,
3166 description: format!("Moved column '{column_name}' left"),
3167 success: true,
3168 }
3169 } else {
3170 ColumnReorderResult {
3171 new_column_position: current_column,
3172 description: "Cannot move column left".to_string(),
3173 success: false,
3174 }
3175 }
3176 }
3177
3178 pub fn reorder_column_right(&mut self, current_column: usize) -> ColumnReorderResult {
3180 let column_count = self.dataview.column_count();
3182
3183 if current_column >= column_count {
3184 return ColumnReorderResult {
3185 new_column_position: current_column,
3186 description: "Invalid column position".to_string(),
3187 success: false,
3188 };
3189 }
3190
3191 let pinned_count = self.dataview.get_pinned_columns().len();
3193
3194 let mut new_dataview = (*self.dataview).clone();
3196
3197 let success = new_dataview.move_column_right(current_column);
3199
3200 if success {
3201 self.dataview = Arc::new(new_dataview);
3203 }
3204
3205 if success {
3206 self.invalidate_cache(); let wrapped_to_beginning = current_column == column_count - 1
3210 || (pinned_count > 0 && current_column == pinned_count - 1);
3211
3212 let new_position = if current_column == column_count - 1 {
3213 if pinned_count > 0 {
3215 pinned_count } else {
3217 0 }
3219 } else if pinned_count > 0 && current_column == pinned_count - 1 {
3220 0
3222 } else {
3223 current_column + 1
3225 };
3226
3227 let column_names = self.dataview.column_names();
3228 let column_name = column_names
3229 .get(new_position)
3230 .map_or("?", std::string::String::as_str);
3231
3232 if wrapped_to_beginning {
3234 self.viewport_cols = 0..self.dataview.column_count().min(20); debug!(target: "viewport_manager",
3237 "Column wrapped to beginning, resetting viewport to show column {} at position {}",
3238 column_name, new_position
3239 );
3240 } else {
3241 if !self.viewport_cols.contains(&new_position) {
3243 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3245
3246 let columns_that_fit =
3248 self.calculate_columns_that_fit(new_position, terminal_width);
3249
3250 let new_start = if new_position > self.viewport_cols.end {
3252 new_position.saturating_sub(columns_that_fit - 1)
3254 } else {
3255 new_position
3257 };
3258
3259 let new_end = (new_start + columns_that_fit).min(self.dataview.column_count());
3260 self.viewport_cols = new_start..new_end;
3261
3262 debug!(target: "viewport_manager",
3263 "Column moved outside viewport! Adjusting viewport to {}..{} to show column {} at position {}",
3264 new_start, new_end, column_name, new_position
3265 );
3266 }
3267 }
3268
3269 self.crosshair_col = new_position;
3271
3272 ColumnReorderResult {
3273 new_column_position: new_position,
3274 description: format!("Moved column '{column_name}' right"),
3275 success: true,
3276 }
3277 } else {
3278 ColumnReorderResult {
3279 new_column_position: current_column,
3280 description: "Cannot move column right".to_string(),
3281 success: false,
3282 }
3283 }
3284 }
3285
3286 pub fn hide_column(&mut self, column_index: usize) -> bool {
3289 debug!(target: "viewport_manager", "hide_column: column_index={}", column_index);
3290
3291 let mut new_dataview = (*self.dataview).clone();
3293
3294 let success = new_dataview.hide_column(column_index);
3296
3297 if success {
3298 self.dataview = Arc::new(new_dataview);
3300 self.invalidate_cache(); let column_count = self.dataview.column_count();
3304 if self.viewport_cols.end > column_count {
3305 self.viewport_cols.end = column_count;
3306 }
3307 if self.viewport_cols.start >= column_count && column_count > 0 {
3308 self.viewport_cols.start = column_count - 1;
3309 }
3310
3311 if column_index == self.crosshair_col {
3314 if column_count > 0 {
3316 if self.crosshair_col >= column_count {
3319 self.crosshair_col = column_count - 1;
3320 }
3321 } else {
3324 self.crosshair_col = 0;
3325 }
3326 debug!(target: "viewport_manager", "Crosshair was on hidden column, moved to {}", self.crosshair_col);
3327 } else if column_index < self.crosshair_col {
3328 self.crosshair_col = self.crosshair_col.saturating_sub(1);
3330 debug!(target: "viewport_manager", "Hidden column was before crosshair, adjusted crosshair to {}", self.crosshair_col);
3331 }
3332
3333 debug!(target: "viewport_manager", "Column {} hidden successfully", column_index);
3334 } else {
3335 debug!(target: "viewport_manager", "Failed to hide column {} (might be pinned)", column_index);
3336 }
3337
3338 success
3339 }
3340
3341 pub fn hide_column_by_name(&mut self, column_name: &str) -> bool {
3344 debug!(target: "viewport_manager", "hide_column_by_name: column_name={}", column_name);
3345
3346 let mut new_dataview = (*self.dataview).clone();
3348
3349 let success = new_dataview.hide_column_by_name(column_name);
3351
3352 if success {
3353 self.dataview = Arc::new(new_dataview);
3355 }
3356
3357 if success {
3358 self.invalidate_cache(); let column_count = self.dataview.column_count();
3362 if self.viewport_cols.end > column_count {
3363 self.viewport_cols.end = column_count;
3364 }
3365 if self.viewport_cols.start >= column_count && column_count > 0 {
3366 self.viewport_cols.start = column_count - 1;
3367 }
3368
3369 if self.crosshair_col >= column_count && column_count > 0 {
3371 self.crosshair_col = column_count - 1;
3372 debug!(target: "viewport_manager", "Adjusted crosshair to {} after hiding column", self.crosshair_col);
3373 }
3374
3375 debug!(target: "viewport_manager", "Column '{}' hidden successfully", column_name);
3376 } else {
3377 debug!(target: "viewport_manager", "Failed to hide column '{}' (might be pinned or not found)", column_name);
3378 }
3379
3380 success
3381 }
3382
3383 pub fn hide_empty_columns(&mut self) -> usize {
3386 debug!(target: "viewport_manager", "hide_empty_columns called");
3387
3388 let mut new_dataview = (*self.dataview).clone();
3390
3391 let count = new_dataview.hide_empty_columns();
3393
3394 if count > 0 {
3395 self.dataview = Arc::new(new_dataview);
3397 }
3398
3399 if count > 0 {
3400 self.invalidate_cache(); let column_count = self.dataview.column_count();
3404 if self.viewport_cols.end > column_count {
3405 self.viewport_cols.end = column_count;
3406 }
3407 if self.viewport_cols.start >= column_count && column_count > 0 {
3408 self.viewport_cols.start = column_count - 1;
3409 }
3410
3411 debug!(target: "viewport_manager", "Hidden {} empty columns", count);
3412 }
3413
3414 count
3415 }
3416
3417 pub fn unhide_all_columns(&mut self) {
3419 debug!(target: "viewport_manager", "unhide_all_columns called");
3420
3421 let mut new_dataview = (*self.dataview).clone();
3423
3424 new_dataview.unhide_all_columns();
3426
3427 self.dataview = Arc::new(new_dataview);
3429
3430 self.invalidate_cache(); let column_count = self.dataview.column_count();
3434 self.viewport_cols = 0..column_count.min(20); debug!(target: "viewport_manager", "All columns unhidden, viewport reset to {:?}", self.viewport_cols);
3437 }
3438
3439 pub fn pin_column(&mut self, column_index: usize) -> bool {
3442 debug!(target: "viewport_manager", "pin_column: column_index={}", column_index);
3443
3444 let mut new_dataview = (*self.dataview).clone();
3446
3447 let success = new_dataview.pin_column(column_index).is_ok();
3449
3450 if success {
3451 self.dataview = Arc::new(new_dataview);
3453 self.invalidate_cache(); debug!(target: "viewport_manager", "Column {} pinned successfully", column_index);
3456 } else {
3457 debug!(target: "viewport_manager", "Failed to pin column {}", column_index);
3458 }
3459
3460 success
3461 }
3462
3463 pub fn set_current_column(&mut self, visual_column: usize) -> bool {
3466 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH); let total_visual_columns = self.dataview.get_display_columns().len();
3468
3469 tracing::debug!("[PIN_DEBUG] === set_current_column ===");
3470 tracing::debug!(
3471 "[PIN_DEBUG] visual_column={}, viewport_cols={:?}",
3472 visual_column,
3473 self.viewport_cols
3474 );
3475 tracing::debug!(
3476 "[PIN_DEBUG] terminal_width={}, total_visual_columns={}",
3477 terminal_width,
3478 total_visual_columns
3479 );
3480
3481 debug!(target: "viewport_manager",
3482 "set_current_column ENTRY: visual_column={}, current_viewport={:?}, terminal_width={}, total_visual={}",
3483 visual_column, self.viewport_cols, terminal_width, total_visual_columns);
3484
3485 if visual_column >= total_visual_columns {
3487 debug!(target: "viewport_manager", "Visual column {} out of bounds (max {})", visual_column, total_visual_columns);
3488 tracing::debug!(
3489 "[PIN_DEBUG] Column {} out of bounds (max {})",
3490 visual_column,
3491 total_visual_columns
3492 );
3493 return false;
3494 }
3495
3496 self.crosshair_col = visual_column;
3498 debug!(target: "viewport_manager", "Updated crosshair_col to {}", visual_column);
3499 tracing::debug!("[PIN_DEBUG] Updated crosshair_col to {}", visual_column);
3500
3501 let display_columns = self.dataview.get_display_columns();
3504 let mut total_width_needed = 0u16;
3505 for &dt_idx in &display_columns {
3506 let width =
3507 self.width_calculator
3508 .get_column_width(&self.dataview, &self.viewport_rows, dt_idx);
3509 total_width_needed += width + 1; }
3511
3512 if total_width_needed <= terminal_width {
3513 debug!(target: "viewport_manager",
3515 "Visual column {} in optimal layout mode (all columns fit), no adjustment needed", visual_column);
3516 tracing::debug!("[PIN_DEBUG] All columns fit, no adjustment needed");
3517 tracing::debug!("[PIN_DEBUG] === End set_current_column (all fit) ===");
3518 return false;
3519 }
3520
3521 let pinned_count = self.dataview.get_pinned_columns().len();
3524 tracing::debug!("[PIN_DEBUG] pinned_count={}", pinned_count);
3525
3526 let visible_columns = self.calculate_visible_column_indices(terminal_width);
3528 let display_columns = self.dataview.get_display_columns();
3529
3530 let target_dt_idx = if visual_column < display_columns.len() {
3532 display_columns[visual_column]
3533 } else {
3534 tracing::debug!("[PIN_DEBUG] Column {} out of bounds", visual_column);
3535 return false;
3536 };
3537
3538 let is_visible = visible_columns.contains(&target_dt_idx);
3539 tracing::debug!(
3540 "[PIN_DEBUG] Column {} (dt_idx={}) visible check: visible_columns={:?}, is_visible={}",
3541 visual_column,
3542 target_dt_idx,
3543 visible_columns,
3544 is_visible
3545 );
3546
3547 debug!(target: "viewport_manager",
3548 "set_current_column CHECK: visual_column={}, viewport={:?}, is_visible={}",
3549 visual_column, self.viewport_cols, is_visible);
3550
3551 if is_visible {
3552 debug!(target: "viewport_manager", "Visual column {} already visible in viewport {:?}, no adjustment needed",
3553 visual_column, self.viewport_cols);
3554 tracing::debug!("[PIN_DEBUG] Column already visible, no adjustment");
3555 tracing::debug!("[PIN_DEBUG] === End set_current_column (no change) ===");
3556 return false;
3557 }
3558
3559 debug!(target: "viewport_manager", "Visual column {} NOT visible, calculating new offset", visual_column);
3561 let new_scroll_offset = self.calculate_scroll_offset_for_visual_column(visual_column);
3562 let old_scroll_offset = self.viewport_cols.start;
3563
3564 debug!(target: "viewport_manager", "Calculated new_scroll_offset={}, old_scroll_offset={}",
3565 new_scroll_offset, old_scroll_offset);
3566
3567 if new_scroll_offset != old_scroll_offset {
3568 let display_columns = self.dataview.get_display_columns();
3571 let pinned_count = self.dataview.get_pinned_columns().len();
3572 let mut used_width = 0u16;
3573 let separator_width = 1u16;
3574
3575 for visual_idx in 0..pinned_count {
3577 if visual_idx < display_columns.len() {
3578 let dt_idx = display_columns[visual_idx];
3579 let width = self.width_calculator.get_column_width(
3580 &self.dataview,
3581 &self.viewport_rows,
3582 dt_idx,
3583 );
3584 used_width += width + separator_width;
3585 }
3586 }
3587
3588 let mut scrollable_columns_that_fit = 0;
3590 let visual_start = pinned_count + new_scroll_offset;
3591
3592 for visual_idx in visual_start..display_columns.len() {
3593 let dt_idx = display_columns[visual_idx];
3594 let width = self.width_calculator.get_column_width(
3595 &self.dataview,
3596 &self.viewport_rows,
3597 dt_idx,
3598 );
3599 if used_width + width + separator_width <= terminal_width {
3600 used_width += width + separator_width;
3601 scrollable_columns_that_fit += 1;
3602 } else {
3603 break;
3604 }
3605 }
3606
3607 let new_end = new_scroll_offset + scrollable_columns_that_fit;
3609 self.viewport_cols = new_scroll_offset..new_end;
3610 self.cache_dirty = true; debug!(target: "viewport_manager",
3613 "Adjusted viewport for visual column {}: offset {}→{} (viewport: {:?})",
3614 visual_column, old_scroll_offset, new_scroll_offset, self.viewport_cols);
3615
3616 return true;
3617 }
3618
3619 false
3620 }
3621
3622 fn calculate_visible_column_indices_with_offset(
3625 &mut self,
3626 available_width: u16,
3627 scroll_offset: usize,
3628 ) -> Vec<usize> {
3629 let original_viewport = self.viewport_cols.clone();
3631 let total_visual_columns = self.dataview.get_display_columns().len();
3632 self.viewport_cols = scroll_offset..(scroll_offset + 50).min(total_visual_columns);
3633
3634 let visible_columns = self.calculate_visible_column_indices(available_width);
3635
3636 self.viewport_cols = original_viewport;
3638
3639 visible_columns
3640 }
3641
3642 fn calculate_scroll_offset_for_visual_column(&mut self, visual_column: usize) -> usize {
3645 debug!(target: "viewport_manager",
3646 "=== calculate_scroll_offset_for_visual_column ENTRY ===");
3647 debug!(target: "viewport_manager",
3648 "visual_column={}, current_viewport={:?}", visual_column, self.viewport_cols);
3649
3650 let pinned_count = self.dataview.get_pinned_columns().len();
3651 debug!(target: "viewport_manager",
3652 "pinned_count={}", pinned_count);
3653
3654 if visual_column < pinned_count {
3656 debug!(target: "viewport_manager",
3657 "Visual column {} is pinned, returning current offset {}",
3658 visual_column, self.viewport_cols.start);
3659 return self.viewport_cols.start; }
3661
3662 let scrollable_column = visual_column - pinned_count;
3664 debug!(target: "viewport_manager",
3665 "Converted to scrollable_column={}", scrollable_column);
3666
3667 let current_scroll_offset = self.viewport_cols.start;
3668 let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3669
3670 let display_columns = self.dataview.get_display_columns();
3672 let mut pinned_width = 0u16;
3673 let separator_width = 1u16;
3674
3675 for visual_idx in 0..pinned_count {
3676 if visual_idx < display_columns.len() {
3677 let dt_idx = display_columns[visual_idx];
3678 let width = self.width_calculator.get_column_width(
3679 &self.dataview,
3680 &self.viewport_rows,
3681 dt_idx,
3682 );
3683 pinned_width += width + separator_width;
3684 }
3685 }
3686
3687 let available_for_scrollable = terminal_width.saturating_sub(pinned_width);
3689
3690 debug!(target: "viewport_manager",
3691 "Scroll offset calculation: target_scrollable_col={}, current_offset={}, available_width={}",
3692 scrollable_column, current_scroll_offset, available_for_scrollable);
3693
3694 if scrollable_column < current_scroll_offset {
3696 debug!(target: "viewport_manager", "Column {} is left of viewport, scrolling left to offset {}",
3698 scrollable_column, scrollable_column);
3699 scrollable_column
3700 } else {
3701 debug!(target: "viewport_manager",
3705 "Checking if column {} can be made visible with minimal scrolling from offset {}",
3706 scrollable_column, current_scroll_offset);
3707
3708 let mut test_scroll_offset = current_scroll_offset;
3710 let max_scrollable_columns = display_columns.len().saturating_sub(pinned_count);
3711
3712 while test_scroll_offset <= scrollable_column
3713 && test_scroll_offset < max_scrollable_columns
3714 {
3715 let mut used_width = 0u16;
3716 let mut target_column_fits = false;
3717
3718 for test_scrollable_idx in test_scroll_offset..max_scrollable_columns {
3720 let visual_idx = pinned_count + test_scrollable_idx;
3721 if visual_idx < display_columns.len() {
3722 let dt_idx = display_columns[visual_idx];
3723 let width = self.width_calculator.get_column_width(
3724 &self.dataview,
3725 &self.viewport_rows,
3726 dt_idx,
3727 );
3728
3729 if used_width + width + separator_width <= available_for_scrollable {
3730 used_width += width + separator_width;
3731 if test_scrollable_idx == scrollable_column {
3732 target_column_fits = true;
3733 break; }
3735 } else {
3736 break; }
3738 }
3739 }
3740
3741 debug!(target: "viewport_manager",
3742 "Testing scroll_offset={}: target_fits={}, used_width={}",
3743 test_scroll_offset, target_column_fits, used_width);
3744
3745 if target_column_fits {
3746 debug!(target: "viewport_manager",
3747 "Found minimal scroll offset {} for column {} (current was {})",
3748 test_scroll_offset, scrollable_column, current_scroll_offset);
3749 return test_scroll_offset;
3750 }
3751
3752 test_scroll_offset += 1;
3754 }
3755
3756 debug!(target: "viewport_manager",
3758 "Could not find minimal scroll, placing column {} at scroll offset {}",
3759 scrollable_column, scrollable_column);
3760 scrollable_column
3761 }
3762 }
3763
3764 pub fn goto_line(&mut self, target_row: usize) -> RowNavigationResult {
3766 let total_rows = self.dataview.row_count();
3767
3768 let target_row = target_row.min(total_rows.saturating_sub(1));
3770
3771 let visible_rows = (self.terminal_height as usize).saturating_sub(6);
3773
3774 let centered_scroll_offset = if visible_rows > 0 {
3776 let half_viewport = visible_rows / 2;
3778 if target_row > half_viewport {
3779 (target_row - half_viewport).min(total_rows.saturating_sub(visible_rows))
3781 } else {
3782 0
3784 }
3785 } else {
3786 target_row
3787 };
3788
3789 let old_scroll_offset = self.viewport_rows.start;
3791 self.viewport_rows =
3792 centered_scroll_offset..(centered_scroll_offset + visible_rows).min(total_rows);
3793 let viewport_changed = centered_scroll_offset != old_scroll_offset;
3794
3795 self.crosshair_row = target_row;
3797
3798 let description = format!(
3799 "Jumped to row {} (centered at viewport {})",
3800 target_row + 1,
3801 centered_scroll_offset + 1
3802 );
3803
3804 debug!(target: "viewport_manager",
3805 "goto_line: target_row={}, crosshair_row={}, scroll_offset={}→{}, viewport={:?}",
3806 target_row, self.crosshair_row, old_scroll_offset, centered_scroll_offset, self.viewport_rows);
3807
3808 RowNavigationResult {
3809 row_position: target_row,
3810 row_scroll_offset: centered_scroll_offset,
3811 description,
3812 viewport_changed,
3813 }
3814 }
3815
3816 pub fn hide_current_column_with_result(&mut self) -> ColumnOperationResult {
3820 let visual_col_idx = self.get_crosshair_col();
3821 let columns = self.dataview.column_names();
3822
3823 if visual_col_idx >= columns.len() {
3824 return ColumnOperationResult::failure("Invalid column position");
3825 }
3826
3827 let col_name = columns[visual_col_idx].clone();
3828 let visible_count = columns.len();
3829
3830 if visible_count <= 1 {
3832 return ColumnOperationResult::failure("Cannot hide the last visible column");
3833 }
3834
3835 let success = self.hide_column(visual_col_idx);
3837
3838 if success {
3839 let mut result = ColumnOperationResult::success(format!("Column '{col_name}' hidden"));
3840 result.updated_dataview = Some(self.clone_dataview());
3841 result.new_column_position = Some(self.get_crosshair_col());
3842 result.new_viewport = Some(self.viewport_cols.clone());
3843 result
3844 } else {
3845 ColumnOperationResult::failure(format!(
3846 "Cannot hide column '{col_name}' (may be pinned)"
3847 ))
3848 }
3849 }
3850
3851 pub fn unhide_all_columns_with_result(&mut self) -> ColumnOperationResult {
3853 let hidden_columns = self.dataview.get_hidden_column_names();
3854 let count = hidden_columns.len();
3855
3856 if count == 0 {
3857 return ColumnOperationResult::success("No hidden columns");
3858 }
3859
3860 self.unhide_all_columns();
3861
3862 let mut result = ColumnOperationResult::success(format!("Unhidden {count} column(s)"));
3863 result.updated_dataview = Some(self.clone_dataview());
3864 result.affected_count = Some(count);
3865 result.new_viewport = Some(self.viewport_cols.clone());
3866 result
3867 }
3868
3869 pub fn reorder_column_left_with_result(&mut self) -> ColumnOperationResult {
3871 let current_col = self.get_crosshair_col();
3872 let reorder_result = self.reorder_column_left(current_col);
3873
3874 if reorder_result.success {
3875 let mut result = ColumnOperationResult::success(reorder_result.description);
3876 result.updated_dataview = Some(self.clone_dataview());
3877 result.new_column_position = Some(reorder_result.new_column_position);
3878 result.new_viewport = Some(self.viewport_cols.clone());
3879 result
3880 } else {
3881 ColumnOperationResult::failure(reorder_result.description)
3882 }
3883 }
3884
3885 pub fn reorder_column_right_with_result(&mut self) -> ColumnOperationResult {
3887 let current_col = self.get_crosshair_col();
3888 let reorder_result = self.reorder_column_right(current_col);
3889
3890 if reorder_result.success {
3891 let mut result = ColumnOperationResult::success(reorder_result.description);
3892 result.updated_dataview = Some(self.clone_dataview());
3893 result.new_column_position = Some(reorder_result.new_column_position);
3894 result.new_viewport = Some(self.viewport_cols.clone());
3895 result
3896 } else {
3897 ColumnOperationResult::failure(reorder_result.description)
3898 }
3899 }
3900
3901 pub fn calculate_viewport_column_widths(
3906 &mut self,
3907 viewport_start: usize,
3908 viewport_end: usize,
3909 compact_mode: bool,
3910 ) -> Vec<u16> {
3911 let headers = self.dataview.column_names();
3912 let mut widths = Vec::with_capacity(headers.len());
3913
3914 let min_width = if compact_mode { 4 } else { 6 };
3916 let padding = if compact_mode { 1 } else { 2 };
3917
3918 let available_width = self.terminal_width.saturating_sub(10) as usize;
3920 let visible_cols = headers.len().min(12); let dynamic_max = if visible_cols > 0 {
3924 (available_width / visible_cols).max(30).min(80)
3925 } else {
3926 30
3927 };
3928
3929 let max_width = if compact_mode {
3930 dynamic_max.min(40)
3931 } else {
3932 dynamic_max
3933 };
3934
3935 let mut rows_to_check = Vec::new();
3937 let source_table = self.dataview.source();
3938 for i in viewport_start..viewport_end.min(source_table.row_count()) {
3939 if let Some(row_strings) = source_table.get_row_as_strings(i) {
3940 rows_to_check.push(row_strings);
3941 }
3942 }
3943
3944 for (col_idx, header) in headers.iter().enumerate() {
3946 let mut max_col_width = header.len();
3948
3949 for row in &rows_to_check {
3951 if let Some(value) = row.get(col_idx) {
3952 let display_value = if value.is_empty() {
3953 "NULL"
3954 } else {
3955 value.as_str()
3956 };
3957 max_col_width = max_col_width.max(display_value.len());
3958 }
3959 }
3960
3961 let width = (max_col_width + padding).clamp(min_width, max_width) as u16;
3963 widths.push(width);
3964 }
3965
3966 widths
3967 }
3968
3969 pub fn calculate_optimal_column_widths(&mut self) -> Vec<u16> {
3972 self.width_calculator.calculate_with_terminal_width(
3974 &self.dataview,
3975 &self.viewport_rows,
3976 self.terminal_width,
3977 );
3978
3979 let col_count = self.dataview.column_count();
3981 let mut widths = Vec::with_capacity(col_count);
3982 for idx in 0..col_count {
3983 widths.push(self.width_calculator.get_column_width(
3984 &self.dataview,
3985 &self.viewport_rows,
3986 idx,
3987 ));
3988 }
3989 widths
3990 }
3991
3992 pub fn ensure_column_visible(&mut self, column_index: usize, available_width: u16) {
3994 debug!(target: "viewport_manager", "ensure_column_visible: column_index={}, available_width={}", column_index, available_width);
3995
3996 let total_columns = self.dataview.get_display_columns().len();
3997
3998 if column_index >= total_columns {
4000 debug!(target: "viewport_manager", "Column index {} out of range (max {})", column_index, total_columns.saturating_sub(1));
4001 return;
4002 }
4003
4004 let visible_columns = self.calculate_visible_column_indices(available_width);
4006 let dt_columns = self.dataview.get_display_columns();
4007
4008 if let Some(&dt_index) = dt_columns.get(column_index) {
4010 if visible_columns.contains(&dt_index) {
4011 debug!(target: "viewport_manager", "Column {} already visible", column_index);
4012 return;
4013 }
4014 }
4015
4016 if self.set_current_column(column_index) {
4019 self.crosshair_col = column_index;
4020 debug!(target: "viewport_manager", "Ensured column {} is visible and set crosshair", column_index);
4021 } else {
4022 debug!(target: "viewport_manager", "Failed to make column {} visible", column_index);
4023 }
4024 }
4025
4026 pub fn reorder_column(&mut self, from_index: usize, to_index: usize) -> bool {
4028 debug!(target: "viewport_manager", "reorder_column: from_index={}, to_index={}", from_index, to_index);
4029
4030 if from_index == to_index {
4031 return true; }
4033
4034 let mut new_dataview = (*self.dataview).clone();
4036
4037 let mut current_pos = from_index;
4038 let mut success = true;
4039
4040 if from_index < to_index {
4042 while current_pos < to_index && success {
4044 success = new_dataview.move_column_right(current_pos);
4045 if success {
4046 current_pos += 1;
4047 }
4048 }
4049 } else {
4050 while current_pos > to_index && success {
4052 success = new_dataview.move_column_left(current_pos);
4053 if success {
4054 current_pos -= 1;
4055 }
4056 }
4057 }
4058
4059 if success {
4060 self.dataview = Arc::new(new_dataview);
4062 self.invalidate_cache(); debug!(target: "viewport_manager", "Column moved from {} to {} successfully", from_index, to_index);
4065 } else {
4066 debug!(target: "viewport_manager", "Failed to move column from {} to {}", from_index, to_index);
4067 }
4068
4069 success
4070 }
4071
4072 pub fn calculate_column_widths(&mut self, available_width: u16) -> Vec<u16> {
4075 let _visible_indices = self.calculate_visible_column_indices(available_width);
4077
4078 self.get_column_widths().to_vec()
4080 }
4081}
4082
4083#[derive(Debug, Clone)]
4085pub struct ViewportEfficiency {
4086 pub available_width: u16,
4087 pub used_width: u16,
4088 pub wasted_space: u16,
4089 pub efficiency_percent: u8,
4090 pub visible_columns: usize,
4091 pub column_widths: Vec<u16>,
4092 pub next_column_width: Option<u16>, pub columns_that_could_fit: Vec<(usize, u16)>, }
4095
4096impl ViewportEfficiency {
4097 #[must_use]
4099 pub fn to_status_string(&self) -> String {
4100 format!(
4101 "Viewport: {}w used of {}w ({}% efficient, {} cols, {}w wasted)",
4102 self.used_width,
4103 self.available_width,
4104 self.efficiency_percent,
4105 self.visible_columns,
4106 self.wasted_space
4107 )
4108 }
4109
4110 #[must_use]
4112 pub fn to_debug_string(&self) -> String {
4113 let avg_width = if self.column_widths.is_empty() {
4114 0
4115 } else {
4116 self.column_widths.iter().sum::<u16>() / self.column_widths.len() as u16
4117 };
4118
4119 let mut efficiency_analysis = String::new();
4121 if let Some(next_width) = self.next_column_width {
4122 efficiency_analysis.push_str(&format!(
4123 "\n\n Next column needs: {next_width}w (+1 separator)"
4124 ));
4125 if next_width < self.wasted_space {
4126 efficiency_analysis.push_str(" ✓ FITS!");
4127 } else {
4128 efficiency_analysis.push_str(&format!(" ✗ Too wide (have {}w)", self.wasted_space));
4129 }
4130 }
4131
4132 if !self.columns_that_could_fit.is_empty() {
4133 efficiency_analysis.push_str(&format!(
4134 "\n Columns that COULD fit in {}w:",
4135 self.wasted_space
4136 ));
4137 for (idx, width) in
4138 &self.columns_that_could_fit[..self.columns_that_could_fit.len().min(5)]
4139 {
4140 efficiency_analysis.push_str(&format!("\n - Column {idx}: {width}w"));
4141 }
4142 if self.columns_that_could_fit.len() > 5 {
4143 efficiency_analysis.push_str(&format!(
4144 "\n ... and {} more",
4145 self.columns_that_could_fit.len() - 5
4146 ));
4147 }
4148 }
4149
4150 efficiency_analysis.push_str("\n\n Hypothetical efficiencies:");
4152 for extra in 1..=3 {
4153 let hypothetical_used =
4154 self.used_width + (extra * (avg_width + 1)).min(self.wasted_space);
4155 let hypothetical_eff =
4156 ((f32::from(hypothetical_used) / f32::from(self.available_width)) * 100.0) as u8;
4157 let hypothetical_wasted = self.available_width.saturating_sub(hypothetical_used);
4158 efficiency_analysis.push_str(&format!(
4159 "\n +{extra} cols ({avg_width}w each): {hypothetical_eff}% efficiency, {hypothetical_wasted}w wasted"
4160 ));
4161 }
4162
4163 format!(
4164 "Viewport Efficiency:\n Available: {}w\n Used: {}w\n Wasted: {}w\n Efficiency: {}%\n Columns: {} visible\n Widths: {:?}\n Avg Width: {}w{}",
4165 self.available_width,
4166 self.used_width,
4167 self.wasted_space,
4168 self.efficiency_percent,
4169 self.visible_columns,
4170 self.column_widths.clone(),
4171 avg_width,
4172 efficiency_analysis
4173 )
4174 }
4175}
4176
4177#[cfg(test)]
4178mod tests {
4179 use super::*;
4180 use crate::data::datatable::{DataColumn, DataRow, DataTable, DataValue};
4181
4182 fn create_test_dataview() -> Arc<DataView> {
4183 let mut table = DataTable::new("test");
4184 table.add_column(DataColumn::new("id"));
4185 table.add_column(DataColumn::new("name"));
4186 table.add_column(DataColumn::new("amount"));
4187
4188 for i in 0..100 {
4189 table
4190 .add_row(DataRow::new(vec![
4191 DataValue::Integer(i),
4192 DataValue::String(format!("Item {i}")),
4193 DataValue::Float(i as f64 * 10.5),
4194 ]))
4195 .unwrap();
4196 }
4197
4198 Arc::new(DataView::new(Arc::new(table)))
4199 }
4200
4201 #[test]
4202 fn test_viewport_basic() {
4203 let dataview = create_test_dataview();
4204 let mut viewport = ViewportManager::new(dataview);
4205
4206 viewport.set_viewport(0, 0, 80, 24);
4207
4208 assert_eq!(viewport.viewport_rows(), 0..24);
4209 assert_eq!(viewport.viewport_cols(), 0..3);
4210
4211 let visible_rows = viewport.get_visible_rows();
4212 assert_eq!(visible_rows.len(), 24);
4213 }
4214
4215 #[test]
4216 fn test_column_width_calculation() {
4217 let dataview = create_test_dataview();
4218 let mut viewport = ViewportManager::new(dataview);
4219
4220 viewport.set_viewport(0, 0, 80, 10);
4221
4222 let widths = viewport.get_column_widths();
4223 assert_eq!(widths.len(), 3);
4224
4225 assert!(widths[0] < 10);
4227 assert!(widths[1] > widths[0]);
4229 }
4230
4231 #[test]
4232 fn test_viewport_scrolling() {
4233 let dataview = create_test_dataview();
4234 let mut viewport = ViewportManager::new(dataview);
4235
4236 viewport.set_viewport(0, 0, 80, 24);
4237 viewport.scroll_by(10, 0);
4238
4239 assert_eq!(viewport.viewport_rows(), 10..34);
4240
4241 viewport.scroll_by(-5, 1);
4242 assert_eq!(viewport.viewport_rows(), 5..29);
4243 assert_eq!(viewport.viewport_cols(), 1..3);
4244 }
4245
4246 #[test]
4249 fn test_navigate_to_last_and_first_column() {
4250 let dataview = create_test_dataview();
4251 let mut vm = ViewportManager::new(dataview);
4252 vm.update_terminal_size(120, 40);
4253
4254 let result = vm.navigate_to_last_column();
4256 assert_eq!(vm.get_crosshair_col(), 2); assert_eq!(result.column_position, 2);
4258
4259 let result = vm.navigate_to_first_column();
4261 assert_eq!(vm.get_crosshair_col(), 0);
4262 assert_eq!(result.column_position, 0);
4263 }
4264
4265 #[test]
4266 fn test_column_reorder_right_with_crosshair() {
4267 let dataview = create_test_dataview();
4268 let mut vm = ViewportManager::new(dataview);
4269 vm.update_terminal_size(120, 40);
4270
4271 vm.crosshair_col = 0;
4273
4274 let result = vm.reorder_column_right(0);
4276 assert!(result.success);
4277 assert_eq!(result.new_column_position, 1);
4278 assert_eq!(vm.get_crosshair_col(), 1); let headers = vm.dataview.column_names();
4282 assert_eq!(headers[0], "name"); assert_eq!(headers[1], "id"); }
4285
4286 #[test]
4287 fn test_column_reorder_left_with_crosshair() {
4288 let dataview = create_test_dataview();
4289 let mut vm = ViewportManager::new(dataview);
4290 vm.update_terminal_size(120, 40);
4291
4292 vm.crosshair_col = 1;
4294
4295 let result = vm.reorder_column_left(1);
4297 assert!(result.success);
4298 assert_eq!(result.new_column_position, 0);
4299 assert_eq!(vm.get_crosshair_col(), 0); }
4301
4302 #[test]
4303 fn test_hide_column_adjusts_crosshair() {
4304 let dataview = create_test_dataview();
4305 let mut vm = ViewportManager::new(dataview);
4306 vm.update_terminal_size(120, 40);
4307
4308 vm.crosshair_col = 1; let success = vm.hide_column(1);
4311 assert!(success);
4312 assert_eq!(vm.get_crosshair_col(), 1);
4314 assert_eq!(vm.dataview.column_count(), 2); vm.crosshair_col = 1; let success = vm.hide_column(1);
4319 assert!(success);
4320 assert_eq!(vm.get_crosshair_col(), 0); assert_eq!(vm.dataview.column_count(), 1); }
4323
4324 #[test]
4325 fn test_goto_line_centers_viewport() {
4326 let dataview = create_test_dataview();
4327 let mut vm = ViewportManager::new(dataview);
4328 vm.update_terminal_size(120, 40);
4329
4330 let result = vm.goto_line(50);
4332 assert_eq!(result.row_position, 50);
4333 assert_eq!(vm.get_crosshair_row(), 50);
4334
4335 let visible_rows = 34; let expected_offset = 50 - (visible_rows / 2);
4338 assert_eq!(result.row_scroll_offset, expected_offset);
4339 }
4340
4341 #[test]
4342 fn test_page_navigation() {
4343 let dataview = create_test_dataview();
4344 let mut vm = ViewportManager::new(dataview);
4345 vm.update_terminal_size(120, 40);
4346
4347 let initial_row = vm.get_crosshair_row();
4349 let result = vm.page_down();
4350 assert!(result.row_position > initial_row);
4351 assert_eq!(vm.get_crosshair_row(), result.row_position);
4352
4353 vm.page_down(); vm.page_down();
4356 let prev_position = vm.get_crosshair_row();
4357 let result = vm.page_up();
4358 assert!(result.row_position < prev_position); }
4360
4361 #[test]
4362 fn test_cursor_lock_mode() {
4363 let dataview = create_test_dataview();
4364 let mut vm = ViewportManager::new(dataview);
4365 vm.update_terminal_size(120, 40);
4366
4367 vm.toggle_cursor_lock();
4369 assert!(vm.is_cursor_locked());
4370
4371 let initial_viewport_position = vm.get_crosshair_row() - vm.viewport_rows.start;
4373 let result = vm.navigate_row_down();
4374
4375 if result.viewport_changed {
4377 let new_viewport_position = vm.get_crosshair_row() - vm.viewport_rows.start;
4378 assert_eq!(initial_viewport_position, new_viewport_position);
4379 }
4380 }
4381
4382 #[test]
4383 fn test_viewport_lock_prevents_scrolling() {
4384 let dataview = create_test_dataview();
4385 let mut vm = ViewportManager::new(dataview);
4386 vm.update_terminal_size(120, 40);
4387
4388 vm.toggle_viewport_lock();
4390 assert!(vm.is_viewport_locked());
4391
4392 let initial_viewport = vm.viewport_rows.clone();
4394 let result = vm.navigate_row_down();
4395
4396 assert_eq!(vm.viewport_rows, initial_viewport);
4398 assert!(!result.viewport_changed);
4400 }
4401
4402 #[test]
4403 fn test_h_m_l_viewport_navigation() {
4404 let dataview = create_test_dataview();
4405 let mut vm = ViewportManager::new(dataview);
4406 vm.update_terminal_size(120, 40);
4407
4408 for _ in 0..20 {
4410 vm.navigate_row_down();
4411 }
4412
4413 let result = vm.navigate_to_viewport_top();
4415 assert_eq!(vm.get_crosshair_row(), vm.viewport_rows.start);
4416
4417 let result = vm.navigate_to_viewport_bottom();
4419 assert_eq!(vm.get_crosshair_row(), vm.viewport_rows.end - 1);
4420
4421 let result = vm.navigate_to_viewport_middle();
4423 let expected_middle =
4424 vm.viewport_rows.start + (vm.viewport_rows.end - vm.viewport_rows.start) / 2;
4425 assert_eq!(vm.get_crosshair_row(), expected_middle);
4426 }
4427
4428 #[test]
4429 fn test_out_of_order_column_navigation() {
4430 let mut table = DataTable::new("test");
4432 for i in 0..12 {
4433 table.add_column(DataColumn::new(format!("col{i}")));
4434 }
4435
4436 for row in 0..10 {
4438 let mut values = Vec::new();
4439 for col in 0..12 {
4440 values.push(DataValue::String(format!("r{row}c{col}")));
4441 }
4442 table.add_row(DataRow::new(values)).unwrap();
4443 }
4444
4445 let dataview =
4449 DataView::new(Arc::new(table)).with_columns(vec![11, 0, 5, 3, 8, 1, 10, 2, 7, 4, 9, 6]);
4450
4451 let mut vm = ViewportManager::new(Arc::new(dataview));
4452 vm.update_terminal_size(200, 40); let column_names = vm.dataview.column_names();
4456 assert_eq!(
4457 column_names[0], "col11",
4458 "First visual column should be col11"
4459 );
4460 assert_eq!(
4461 column_names[1], "col0",
4462 "Second visual column should be col0"
4463 );
4464 assert_eq!(
4465 column_names[2], "col5",
4466 "Third visual column should be col5"
4467 );
4468
4469 vm.crosshair_col = 0;
4471
4472 let mut visual_positions = vec![0];
4474 let mut datatable_positions = vec![];
4475
4476 let display_cols = vm.dataview.get_display_columns();
4478 datatable_positions.push(display_cols[0]);
4479
4480 for i in 0..11 {
4482 let current_visual = vm.get_crosshair_col();
4483 let result = vm.navigate_column_right(current_visual);
4484
4485 let new_visual = vm.get_crosshair_col();
4487 assert_eq!(
4488 new_visual,
4489 current_visual + 1,
4490 "Crosshair should move from visual position {} to {}, but got {}",
4491 current_visual,
4492 current_visual + 1,
4493 new_visual
4494 );
4495
4496 visual_positions.push(new_visual);
4497 let display_cols = vm.dataview.get_display_columns();
4499 datatable_positions.push(display_cols[new_visual]);
4500 }
4501
4502 assert_eq!(
4504 visual_positions,
4505 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
4506 "Crosshair should move through visual positions sequentially"
4507 );
4508
4509 assert_eq!(
4511 datatable_positions,
4512 vec![11, 0, 5, 3, 8, 1, 10, 2, 7, 4, 9, 6],
4513 "DataTable indices should match our column selection order"
4514 );
4515
4516 for _i in (0..11).rev() {
4518 let current_visual = vm.get_crosshair_col();
4519 let _result = vm.navigate_column_left(current_visual);
4520
4521 let new_visual = vm.get_crosshair_col();
4523 assert_eq!(
4524 new_visual,
4525 current_visual - 1,
4526 "Crosshair should move from visual position {} to {}, but got {}",
4527 current_visual,
4528 current_visual - 1,
4529 new_visual
4530 );
4531 }
4532
4533 assert_eq!(
4535 vm.get_crosshair_col(),
4536 0,
4537 "Should be back at first visual column"
4538 );
4539
4540 vm.hide_column(2); vm.crosshair_col = 0;
4545 let _result1 = vm.navigate_column_right(0);
4546 assert_eq!(vm.get_crosshair_col(), 1, "Should be at visual position 1");
4547
4548 let _result2 = vm.navigate_column_right(1);
4549 assert_eq!(
4550 vm.get_crosshair_col(),
4551 2,
4552 "Should be at visual position 2 after hiding"
4553 );
4554
4555 let visible_cols = vm.dataview.column_names();
4557 assert_eq!(
4558 visible_cols[2], "col3",
4559 "Column at position 2 should be col3 after hiding col5"
4560 );
4561 }
4562}