diff --git a/crates/nu-explore/src/views/binary/mod.rs b/crates/nu-explore/src/views/binary/mod.rs index 7e44a3985e..bc75002223 100644 --- a/crates/nu-explore/src/views/binary/mod.rs +++ b/crates/nu-explore/src/views/binary/mod.rs @@ -17,16 +17,20 @@ use crate::{ ConfigMap, Frame, Transition, ViewInfo, }, util::create_map, + views::cursor::Position, }; use self::binary_widget::{BinarySettings, BinaryStyle, BinaryWidget}; -use super::{cursor::XYCursor, Layout, View, ViewConfig}; +use super::{cursor::WindowCursor2D, Layout, View, ViewConfig}; +/// An interactive view that displays binary data in a hex dump format. +/// Not finished; many aspects are still WIP. #[derive(Debug, Clone)] pub struct BinaryView { data: Vec, - cursor: XYCursor, + // HACK: we are only using the vertical dimension of the cursor, should we use a plain old WindowCursor? + cursor: WindowCursor2D, settings: Settings, } @@ -40,7 +44,7 @@ impl BinaryView { pub fn new(data: Vec) -> Self { Self { data, - cursor: XYCursor::default(), + cursor: WindowCursor2D::default(), settings: Settings::default(), } } @@ -95,12 +99,13 @@ impl View for BinaryView { let count_rows = BinaryWidget::new(&self.data, self.settings.opts, Default::default()).count_lines(); - self.cursor = XYCursor::new(count_rows, 0); + // TODO: refactor View so setup() is fallible and we don't have to panic here + self.cursor = WindowCursor2D::new(count_rows, 1).expect("Failed to create XYCursor"); } } fn create_binary_widget(v: &BinaryView) -> BinaryWidget<'_> { - let start_line = v.cursor.row_starts_at(); + let start_line = v.cursor.window_origin().row; let count_elements = BinaryWidget::new(&[], v.settings.opts, Default::default()).count_elements(); let index = start_line * count_elements; @@ -203,7 +208,7 @@ fn config_get_usize(config: &ConfigMap, key: &str, default: usize) -> usize { .unwrap_or(default) } -fn create_report(cursor: XYCursor) -> Report { +fn create_report(cursor: WindowCursor2D) -> Report { let covered_percent = report_row_position(cursor); let cursor = report_cursor_position(cursor); let mode = report_mode_name(); @@ -216,8 +221,8 @@ fn report_mode_name() -> String { String::from("VIEW") } -fn report_row_position(cursor: XYCursor) -> String { - if cursor.row_starts_at() == 0 { +fn report_row_position(cursor: WindowCursor2D) -> String { + if cursor.window_origin().row == 0 { return String::from("Top"); } @@ -233,10 +238,9 @@ fn report_row_position(cursor: XYCursor) -> String { } } -fn report_cursor_position(cursor: XYCursor) -> String { - let rows_seen = cursor.row_starts_at(); - let columns_seen = cursor.column_starts_at(); - format!("{rows_seen},{columns_seen}") +fn report_cursor_position(cursor: WindowCursor2D) -> String { + let Position { row, column } = cursor.window_origin(); + format!("{row},{column}") } fn get_percentage(value: usize, max: usize) -> usize { diff --git a/crates/nu-explore/src/views/cursor/mod.rs b/crates/nu-explore/src/views/cursor/mod.rs index bb2aed5f18..ea08da0d36 100644 --- a/crates/nu-explore/src/views/cursor/mod.rs +++ b/crates/nu-explore/src/views/cursor/mod.rs @@ -1,71 +1,125 @@ -mod windowcursor; -mod xycursor; +mod window_cursor; +mod window_cursor_2d; -pub use windowcursor::WindowCursor; -pub use xycursor::XYCursor; +use anyhow::{bail, Result}; +pub use window_cursor::WindowCursor; +pub use window_cursor_2d::{Position, WindowCursor2D}; +/// A 1-dimensional cursor to track a position from 0 to N +/// +/// Say we have a cursor with size=9, at position 3: +/// 0 1 2 3 4 5 6 7 8 9 +/// | | | C | | | | | | +/// +/// After moving forward by 2 steps: +/// 0 1 2 3 4 5 6 7 8 9 +/// | | | | | C | | | | +/// +/// After moving backward by 6 steps (clamped to 0): +/// 0 1 2 3 4 5 6 7 8 9 +/// C | | | | | | | | | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Cursor { - index: usize, - limit: usize, +struct Cursor { + /// The current position of the cursor + position: usize, + /// The number of distinct positions the cursor can be at + size: usize, } impl Cursor { - pub fn new(limit: usize) -> Self { - Self { index: 0, limit } - } - - #[allow(dead_code)] - pub fn get_index(&self) -> usize { - self.index - } - - pub fn cap(&self) -> usize { - self.limit - self.index - } - - pub fn set(&mut self, i: usize) -> bool { - if i >= self.limit { - return false; + /// Constructor to create a new Cursor + pub fn new(size: usize) -> Result { + if size == 0 { + bail!("Size cannot be zero"); } - - self.index = i; - true - } - - pub fn limit(&mut self, i: usize) -> bool { - if self.index > self.limit { - self.index = self.limit.saturating_sub(1); - return false; - } - - self.limit = i; - if self.index >= self.limit { - self.index = self.limit.saturating_sub(1); - } - - true + Ok(Cursor { position: 0, size }) } + /// The max position the cursor can be at pub fn end(&self) -> usize { - self.limit + self.size - 1 } - pub fn next(&mut self, i: usize) -> bool { - if self.index + i == self.limit { - return false; + /// Set the position to a specific value within the bounds [0, end] + pub fn set_position(&mut self, pos: usize) { + if pos <= self.end() { + self.position = pos; + } else { + // Clamp the position to end if out of bounds + self.position = self.end(); } - - self.index += i; - true } - pub fn prev(&mut self, i: usize) -> bool { - if self.index < i { - return false; + /// Set the size of the cursor. The position is clamped if it exceeds the new size + pub fn set_size(&mut self, size: usize) -> Result<()> { + if size == 0 { + bail!("Size cannot be zero"); } + self.size = size; + if self.position > self.end() { + self.position = self.end(); + } + Ok(()) + } - self.index -= i; - true + /// Move the cursor forward by a specified number of steps + pub fn move_forward(&mut self, steps: usize) { + if self.position + steps <= self.end() { + self.position += steps; + } else { + self.position = self.end(); + } + } + + /// Move the cursor backward by a specified number of steps + pub fn move_backward(&mut self, steps: usize) { + if self.position >= steps { + self.position -= steps; + } else { + self.position = 0; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cursor_set_position() { + // from 0 to 9 + let mut cursor = Cursor::new(10).unwrap(); + cursor.set_position(5); + assert_eq!(cursor.position, 5); + + cursor.set_position(15); + assert_eq!(cursor.position, 9); + } + + #[test] + fn test_cursor_move_forward() { + // from 0 to 9 + let mut cursor = Cursor::new(10).unwrap(); + assert_eq!(cursor.position, 0); + cursor.move_forward(3); + assert_eq!(cursor.position, 3); + + cursor.move_forward(10); + assert_eq!(cursor.position, 9); + } + + #[test] + fn test_cursor_move_backward() { + // from 0 to 9 + let mut cursor = Cursor::new(10).unwrap(); + cursor.move_backward(3); + assert_eq!(cursor.position, 0); + + cursor.move_forward(5); + assert_eq!(cursor.position, 5); + cursor.move_backward(3); + assert_eq!(cursor.position, 2); + cursor.move_backward(3); + assert_eq!(cursor.position, 0); } } diff --git a/crates/nu-explore/src/views/cursor/window_cursor.rs b/crates/nu-explore/src/views/cursor/window_cursor.rs new file mode 100644 index 0000000000..4feb0c802f --- /dev/null +++ b/crates/nu-explore/src/views/cursor/window_cursor.rs @@ -0,0 +1,146 @@ +use std::cmp::min; + +use super::Cursor; +use anyhow::{bail, Ok, Result}; + +/// WindowCursor provides a mechanism to navigate through a 1-dimensional range +/// using a smaller movable window within the view. +/// +/// View: The larger context or total allowable range for navigation. +/// Window: The smaller, focused subset of the view. +/// +/// Example: +/// ```plaintext +/// 1. Initial view of size 20 with a window of size 5. The absolute cursor position starts at 0. +/// View : +/// |--------------------| +/// Window : +/// |X====| +/// +/// 2. After advancing the window by 3, the absolute cursor position becomes 3. +/// View : +/// |--------------------| +/// Window : +/// |X====| +/// +/// 3. After advancing the cursor inside the window by 2, the absolute cursor position becomes 5. +/// View : +/// |--------------------| +/// Window : +/// |==X==| +/// ``` +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct WindowCursor { + view: Cursor, + window: Cursor, +} + +impl WindowCursor { + pub fn new(view_size: usize, window_size: usize) -> Result { + if window_size > view_size { + bail!("Window size cannot be greater than view size"); + } + + Ok(Self { + view: Cursor::new(view_size)?, + window: Cursor::new(window_size)?, + }) + } + + pub fn absolute_position(&self) -> usize { + self.window_starts_at() + self.window.position + } + + pub fn window_relative_position(&self) -> usize { + self.window.position + } + + pub fn window_starts_at(&self) -> usize { + self.view.position + } + + pub fn window_size(&self) -> usize { + self.window.size + } + + pub fn end(&self) -> usize { + self.view.end() + } + + pub fn set_window_start_position(&mut self, i: usize) { + self.view.set_position(i) + } + + pub fn move_window_to_end(&mut self) { + self.view.set_position(self.end() - self.window_size() + 1); + } + + pub fn set_window_size(&mut self, new_size: usize) -> Result<()> { + if new_size > self.view.size { + // TODO: should we return an error here or clamp? the Ok is copying existing behavior + return Ok(()); + } + + self.window.set_size(new_size)?; + Ok(()) + } + + pub fn next_n(&mut self, n: usize) { + for _ in 0..n { + self.next(); + } + } + + pub fn next(&mut self) { + if self.absolute_position() >= self.end() { + return; + } + + if self.window_relative_position() == self.window.end() { + self.view.move_forward(1); + } else { + self.window.move_forward(1); + } + } + + pub fn next_window(&mut self) { + self.move_cursor_to_end_of_window(); + + // move window forward by window size, or less if that would send it off the end of the view + let window_end = self.window_starts_at() + self.window_size() - 1; + let distance_from_window_end_to_view_end = self.end() - window_end; + self.view.move_forward(min( + distance_from_window_end_to_view_end, + self.window_size(), + )); + } + + pub fn prev_n(&mut self, n: usize) { + for _ in 0..n { + self.prev(); + } + } + + pub fn prev(&mut self) { + if self.window_relative_position() == 0 { + self.view.move_backward(1); + } else { + self.window.move_backward(1); + } + } + + pub fn prev_window(&mut self) { + self.move_cursor_to_start_of_window(); + + // move the whole window back + self.view.move_backward(self.window_size()); + } + + pub fn move_cursor_to_start_of_window(&mut self) { + self.window.move_backward(self.window_size()); + } + + pub fn move_cursor_to_end_of_window(&mut self) { + self.window.move_forward(self.window_size()); + } +} diff --git a/crates/nu-explore/src/views/cursor/window_cursor_2d.rs b/crates/nu-explore/src/views/cursor/window_cursor_2d.rs new file mode 100644 index 0000000000..e22ec09ce3 --- /dev/null +++ b/crates/nu-explore/src/views/cursor/window_cursor_2d.rs @@ -0,0 +1,172 @@ +use super::WindowCursor; +use anyhow::Result; + +/// `WindowCursor2D` manages a 2-dimensional "window" onto a grid of cells, with a cursor that can point to a specific cell. +/// For example, consider a 3x3 grid of cells: +/// +/// +---+---+---+ +/// | a | b | c | +/// |---|---|---| +/// | d | e | f | +/// |---|---|---| +/// | g | h | i | +/// +---+---+---+ +/// +/// A `WindowCursor2D` can be used to track the currently visible section of this grid. +/// For example, a 2x2 window onto this grid could initially show the top left 2x2 section: +/// +/// +---+---+ +/// | a | b | +/// |---|---| +/// | d | e | +/// +---+---+ +/// +/// Moving the window down 1 row: +/// +/// +---+---+ +/// | d | e | +/// |---|---| +/// | g | h | +/// +---+---+ +/// +/// Inside the window, the cursor can point to a specific cell. +#[derive(Debug, Default, Clone, Copy)] +pub struct WindowCursor2D { + x: WindowCursor, + y: WindowCursor, +} + +pub struct Position { + pub row: usize, + pub column: usize, +} + +impl WindowCursor2D { + pub fn new(count_rows: usize, count_columns: usize) -> Result { + Ok(Self { + x: WindowCursor::new(count_columns, count_columns)?, + y: WindowCursor::new(count_rows, count_rows)?, + }) + } + + pub fn set_window_size(&mut self, count_rows: usize, count_columns: usize) -> Result<()> { + self.x.set_window_size(count_columns)?; + self.y.set_window_size(count_rows)?; + Ok(()) + } + + pub fn set_window_start_position(&mut self, row: usize, col: usize) { + self.x.set_window_start_position(col); + self.y.set_window_start_position(row); + } + + /// The absolute position of the cursor in the grid (0-indexed, row only) + pub fn row(&self) -> usize { + self.y.absolute_position() + } + + /// The absolute position of the cursor in the grid (0-indexed, column only) + pub fn column(&self) -> usize { + self.x.absolute_position() + } + + /// The absolute position of the cursor in the grid (0-indexed) + pub fn position(&self) -> Position { + Position { + row: self.row(), + column: self.column(), + } + } + + pub fn row_limit(&self) -> usize { + self.y.end() + } + + pub fn window_origin(&self) -> Position { + Position { + row: self.y.window_starts_at(), + column: self.x.window_starts_at(), + } + } + + pub fn window_relative_position(&self) -> Position { + Position { + row: self.y.window_relative_position(), + column: self.x.window_relative_position(), + } + } + + pub fn window_width_in_columns(&self) -> usize { + self.x.window_size() + } + + pub fn next_row(&mut self) { + self.y.next_n(1) + } + + pub fn next_row_page(&mut self) { + self.y.next_window() + } + + pub fn row_move_to_end(&mut self) { + self.y.move_window_to_end(); + self.y.move_cursor_to_end_of_window(); + } + + pub fn row_move_to_start(&mut self) { + self.y.move_cursor_to_start_of_window(); + self.y.set_window_start_position(0); + } + + pub fn prev_row(&mut self) { + self.y.prev() + } + + pub fn prev_row_page(&mut self) { + self.y.prev_window() + } + + pub fn next_column(&mut self) { + self.x.next() + } + + pub fn next_column_by(&mut self, i: usize) { + self.x.next_n(i) + } + + pub fn prev_column(&mut self) { + self.x.prev() + } + + pub fn prev_column_by(&mut self, i: usize) { + self.x.prev_n(i) + } + + pub fn next_column_i(&mut self) { + self.x + .set_window_start_position(self.x.window_starts_at() + 1) + } + + pub fn prev_column_i(&mut self) { + if self.x.window_starts_at() == 0 { + return; + } + + self.x + .set_window_start_position(self.x.window_starts_at() - 1) + } + + pub fn next_row_i(&mut self) { + self.y + .set_window_start_position(self.y.window_starts_at() + 1) + } + + pub fn prev_row_i(&mut self) { + if self.y.window_starts_at() == 0 { + return; + } + + self.y + .set_window_start_position(self.y.window_starts_at() - 1) + } +} diff --git a/crates/nu-explore/src/views/cursor/windowcursor.rs b/crates/nu-explore/src/views/cursor/windowcursor.rs deleted file mode 100644 index a223fb97cb..0000000000 --- a/crates/nu-explore/src/views/cursor/windowcursor.rs +++ /dev/null @@ -1,106 +0,0 @@ -use super::Cursor; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct WindowCursor { - view: Cursor, - window: Cursor, -} - -impl WindowCursor { - pub fn new(limit: usize, window: usize) -> Option { - if window > limit { - return None; - } - - Some(Self { - view: Cursor::new(limit), - window: Cursor::new(window), - }) - } - - pub fn index(&self) -> usize { - self.view.index + self.window.index - } - - pub fn offset(&self) -> usize { - self.window.index - } - - pub fn starts_at(&self) -> usize { - self.view.index - } - - pub fn cap(&self) -> usize { - self.view.cap() - } - - pub fn window(&self) -> usize { - self.window.end() - } - - pub fn end(&self) -> usize { - self.view.end() - } - - pub fn set_window_at(&mut self, i: usize) -> bool { - self.view.set(i) - } - - pub fn set_window(&mut self, i: usize) -> bool { - if i > self.view.end() { - return false; - } - - self.window.limit(i) - } - - pub fn next(&mut self, i: usize) -> bool { - if i > self.cap() { - return false; - } - - let mut rest = 0; - for y in 0..i { - if !self.window.next(1) { - rest = i - y; - break; - } - } - - for _ in 0..rest { - if self.index() + 1 == self.end() { - return rest != i; - } - - self.view.next(1); - } - - true - } - - pub fn next_window(&mut self) -> bool { - let end_cursor = self.window() - self.offset(); - self.next(end_cursor); - - let mut index_move = self.window(); - if index_move + self.starts_at() >= self.end() { - index_move = self.end() - self.starts_at(); - } - - self.next(index_move) - } - - pub fn prev(&mut self, i: usize) -> bool { - for _ in 0..i { - if !self.window.prev(1) { - self.view.prev(1); - } - } - - true - } - - pub fn prev_window(&mut self) -> bool { - self.prev(self.window() + self.offset()) - } -} diff --git a/crates/nu-explore/src/views/cursor/xycursor.rs b/crates/nu-explore/src/views/cursor/xycursor.rs deleted file mode 100644 index 82ca9422c8..0000000000 --- a/crates/nu-explore/src/views/cursor/xycursor.rs +++ /dev/null @@ -1,122 +0,0 @@ -use super::WindowCursor; - -#[derive(Debug, Default, Clone, Copy)] -pub struct XYCursor { - x: WindowCursor, - y: WindowCursor, -} - -impl XYCursor { - pub fn new(count_rows: usize, count_columns: usize) -> Self { - Self { - x: WindowCursor::new(count_columns, count_columns).expect("..."), - y: WindowCursor::new(count_rows, count_rows).expect("..."), - } - } - - pub fn set_window(&mut self, count_rows: usize, count_columns: usize) { - self.x.set_window(count_columns); - self.y.set_window(count_rows); - } - - pub fn set_position(&mut self, row: usize, col: usize) { - self.x.set_window_at(col); - self.y.set_window_at(row); - } - - pub fn row(&self) -> usize { - self.y.index() - } - - pub fn column(&self) -> usize { - self.x.index() - } - - pub fn row_limit(&self) -> usize { - self.y.end() - } - - pub fn row_starts_at(&self) -> usize { - self.y.starts_at() - } - - pub fn column_starts_at(&self) -> usize { - self.x.starts_at() - } - - pub fn row_window(&self) -> usize { - self.y.offset() - } - - pub fn column_window(&self) -> usize { - self.x.offset() - } - - pub fn column_window_size(&self) -> usize { - self.x.window() - } - - pub fn next_row(&mut self) -> bool { - self.y.next(1) - } - - pub fn next_row_page(&mut self) -> bool { - self.y.next_window() - } - - pub fn row_move_to_end(&mut self) -> bool { - self.y.next(self.y.cap()) - } - - pub fn row_move_to_start(&mut self) -> bool { - self.y.prev(self.y.index()) - } - - pub fn prev_row(&mut self) -> bool { - self.y.prev(1) - } - - pub fn prev_row_page(&mut self) -> bool { - self.y.prev_window() - } - - pub fn next_column(&mut self) -> bool { - self.x.next(1) - } - - pub fn next_column_by(&mut self, i: usize) -> bool { - self.x.next(i) - } - - pub fn prev_column(&mut self) -> bool { - self.x.prev(1) - } - - pub fn prev_column_by(&mut self, i: usize) -> bool { - self.x.prev(i) - } - - pub fn next_column_i(&mut self) -> bool { - self.x.set_window_at(self.x.starts_at() + 1) - } - - pub fn prev_column_i(&mut self) -> bool { - if self.x.starts_at() == 0 { - return false; - } - - self.x.set_window_at(self.x.starts_at() - 1) - } - - pub fn next_row_i(&mut self) -> bool { - self.y.set_window_at(self.y.starts_at() + 1) - } - - pub fn prev_row_i(&mut self) -> bool { - if self.y.starts_at() == 0 { - return false; - } - - self.y.set_window_at(self.y.starts_at() - 1) - } -} diff --git a/crates/nu-explore/src/views/interactive.rs b/crates/nu-explore/src/views/interactive.rs index 3e9de30676..d95a78f85e 100644 --- a/crates/nu-explore/src/views/interactive.rs +++ b/crates/nu-explore/src/views/interactive.rs @@ -160,7 +160,7 @@ impl View for InteractiveView<'_> { .as_mut() .expect("we know that we have a table cause of a flag"); - let was_at_the_top = table.get_current_position().0 == 0; + let was_at_the_top = table.get_cursor_position().row == 0; if was_at_the_top && matches!(key.code, KeyCode::Up | KeyCode::PageUp) { self.view_mode = false; diff --git a/crates/nu-explore/src/views/preview.rs b/crates/nu-explore/src/views/preview.rs index 360cfbd226..de4d710113 100644 --- a/crates/nu-explore/src/views/preview.rs +++ b/crates/nu-explore/src/views/preview.rs @@ -1,4 +1,6 @@ -use super::{colored_text_widget::ColoredTextWidget, cursor::XYCursor, Layout, View, ViewConfig}; +use super::{ + colored_text_widget::ColoredTextWidget, cursor::WindowCursor2D, Layout, View, ViewConfig, +}; use crate::{ nu_common::{NuSpan, NuText}, pager::{report::Report, Frame, Transition, ViewInfo}, @@ -17,7 +19,7 @@ use std::cmp::max; pub struct Preview { underlying_value: Option, lines: Vec, - cursor: XYCursor, + cursor: WindowCursor2D, } impl Preview { @@ -26,8 +28,9 @@ impl Preview { .lines() .map(|line| line.replace('\t', " ")) // tui: doesn't support TAB .collect(); - let cursor = XYCursor::new(lines.len(), usize::MAX); + // TODO: refactor so this is fallible and returns a Result instead of panicking + let cursor = WindowCursor2D::new(lines.len(), usize::MAX).expect("Failed to create cursor"); Self { lines, cursor, @@ -38,10 +41,11 @@ impl Preview { impl View for Preview { fn draw(&mut self, f: &mut Frame, area: Rect, _: ViewConfig<'_>, layout: &mut Layout) { - self.cursor - .set_window(area.height as usize, area.width as usize); + let _ = self + .cursor + .set_window_size(area.height as usize, area.width as usize); - let lines = &self.lines[self.cursor.row_starts_at()..]; + let lines = &self.lines[self.cursor.window_origin().row..]; for (i, line) in lines.iter().enumerate().take(area.height as usize) { let text_widget = ColoredTextWidget::new(line, self.cursor.column()); let plain_text = text_widget.get_plain_text(area.width as usize); @@ -65,13 +69,13 @@ impl View for Preview { match key.code { KeyCode::Left => { self.cursor - .prev_column_by(max(1, self.cursor.column_window_size() / 2)); + .prev_column_by(max(1, self.cursor.window_width_in_columns() / 2)); Some(Transition::Ok) } KeyCode::Right => { self.cursor - .next_column_by(max(1, self.cursor.column_window_size() / 2)); + .next_column_by(max(1, self.cursor.window_width_in_columns() / 2)); Some(Transition::Ok) } @@ -128,7 +132,7 @@ impl View for Preview { // // todo: improve somehow? - self.cursor.set_position(row, 0); + self.cursor.set_window_start_position(row, 0); true } @@ -152,7 +156,7 @@ fn set_status_end(view: &Preview, info: &mut ViewInfo) { } fn set_status_top(view: &Preview, info: &mut ViewInfo) { - if view.cursor.row_starts_at() == 0 { + if view.cursor.window_origin().row == 0 { info.status = Some(Report::info("TOP")); } else { info.status = Some(Report::default()); diff --git a/crates/nu-explore/src/views/record/mod.rs b/crates/nu-explore/src/views/record/mod.rs index 5bd74c2329..b3cd93cfd3 100644 --- a/crates/nu-explore/src/views/record/mod.rs +++ b/crates/nu-explore/src/views/record/mod.rs @@ -2,7 +2,7 @@ mod table_widget; use self::table_widget::{TableStyle, TableWidget, TableWidgetState}; use super::{ - cursor::XYCursor, + cursor::{Position, WindowCursor2D}, util::{make_styled_string, nu_style_to_tui}, Layout, View, ViewConfig, }; @@ -133,22 +133,22 @@ impl<'a> RecordView<'a> { layer.reset_cursor(); } - pub fn get_current_position(&self) -> (usize, usize) { + /// Get the current position of the cursor in the table as a whole + pub fn get_cursor_position(&self) -> Position { let layer = self.get_layer_last(); - (layer.cursor.row(), layer.cursor.column()) + layer.cursor.position() } - pub fn get_current_window(&self) -> (usize, usize) { + /// Get the current position of the cursor in the window being shown + pub fn get_cursor_position_in_window(&self) -> Position { let layer = self.get_layer_last(); - (layer.cursor.row_window(), layer.cursor.column_window()) + layer.cursor.window_relative_position() } - pub fn get_current_offset(&self) -> (usize, usize) { + /// Get the origin of the window being shown. (0,0), top left corner. + pub fn get_window_origin(&self) -> Position { let layer = self.get_layer_last(); - ( - layer.cursor.row_starts_at(), - layer.cursor.column_starts_at(), - ) + layer.cursor.window_origin() } pub fn set_cursor_mode(&mut self) { @@ -160,7 +160,7 @@ impl<'a> RecordView<'a> { } pub fn get_current_value(&self) -> Value { - let (row, column) = self.get_current_position(); + let Position { row, column } = self.get_cursor_position(); let layer = self.get_layer_last(); let (row, column) = match layer.orientation { @@ -175,15 +175,17 @@ impl<'a> RecordView<'a> { } } - fn create_tablew(&'a self, cfg: ViewConfig<'a>) -> TableWidget<'a> { + /// Create a table widget. + /// WARNING: this is currently really slow on large data sets. + /// It creates a string representation of every cell in the table and looks at every row for lscolorize. + fn create_table_widget(&'a self, cfg: ViewConfig<'a>) -> TableWidget<'a> { let layer = self.get_layer_last(); let mut data = convert_records_to_string(&layer.records, cfg.nu_config, cfg.style_computer); - lscolorize(&layer.columns, &mut data, cfg.lscolors); let headers = layer.columns.as_ref(); let style_computer = cfg.style_computer; - let (row, column) = self.get_current_offset(); + let Position { row, column } = self.get_window_origin(); TableWidget::new( headers, @@ -199,11 +201,17 @@ impl<'a> RecordView<'a> { fn update_cursors(&mut self, rows: usize, columns: usize) { match self.get_layer_last().orientation { Orientation::Top => { - self.get_layer_last_mut().cursor.set_window(rows, columns); + let _ = self + .get_layer_last_mut() + .cursor + .set_window_size(rows, columns); } Orientation::Left => { - self.get_layer_last_mut().cursor.set_window(rows, columns); + let _ = self + .get_layer_last_mut() + .cursor + .set_window_size(rows, columns); } } } @@ -226,7 +234,10 @@ impl<'a> RecordView<'a> { impl View for RecordView<'_> { fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) { let mut table_layout = TableWidgetState::default(); - let table = self.create_tablew(cfg); + // TODO: creating the table widget is O(N) where N is the number of cells in the grid. + // Way too slow to do on every draw call! + // To make explore work for larger data sets, this needs to be improved. + let table = self.create_table_widget(cfg); f.render_stateful_widget(table, area, &mut table_layout); *layout = table_layout.layout; @@ -234,7 +245,7 @@ impl View for RecordView<'_> { self.update_cursors(table_layout.count_rows, table_layout.count_columns); if self.mode == UIMode::Cursor { - let (row, column) = self.get_current_window(); + let Position { row, column } = self.get_cursor_position_in_window(); let info = get_element_info( layout, row, @@ -308,7 +319,9 @@ impl View for RecordView<'_> { for (column, _) in cells.iter().enumerate() { if i == pos { - self.get_layer_last_mut().cursor.set_position(row, column); + self.get_layer_last_mut() + .cursor + .set_window_start_position(row, column); return true; } @@ -374,7 +387,7 @@ pub struct RecordLayer<'a> { orientation: Orientation, name: Option, was_transposed: bool, - cursor: XYCursor, + cursor: WindowCursor2D, } impl<'a> RecordLayer<'a> { @@ -384,7 +397,10 @@ impl<'a> RecordLayer<'a> { ) -> Self { let columns = columns.into(); let records = records.into(); - let cursor = XYCursor::new(records.len(), columns.len()); + + // TODO: refactor so this is fallible and returns a Result instead of panicking + let cursor = + WindowCursor2D::new(records.len(), columns.len()).expect("Failed to create cursor"); Self { columns, @@ -420,7 +436,9 @@ impl<'a> RecordLayer<'a> { } fn reset_cursor(&mut self) { - self.cursor = XYCursor::new(self.count_rows(), self.count_columns()); + // TODO: refactor so this is fallible and returns a Result instead of panicking + self.cursor = WindowCursor2D::new(self.count_rows(), self.count_columns()) + .expect("Failed to create cursor"); } } @@ -634,7 +652,9 @@ fn tail_data(state: &mut RecordView<'_>, page_size: usize) { let layer = state.get_layer_last_mut(); let count_rows = layer.records.len(); if count_rows > page_size { - layer.cursor.set_position(count_rows - page_size, 0); + layer + .cursor + .set_window_start_position(count_rows - page_size, 0); } } @@ -731,20 +751,18 @@ fn build_table_as_record(v: &RecordView) -> Value { Value::record(record, NuSpan::unknown()) } -fn report_cursor_position(mode: UIMode, cursor: XYCursor) -> String { +fn report_cursor_position(mode: UIMode, cursor: WindowCursor2D) -> String { if mode == UIMode::Cursor { - let row = cursor.row(); - let column = cursor.column(); + let Position { row, column } = cursor.position(); format!("{row},{column}") } else { - let rows_seen = cursor.row_starts_at(); - let columns_seen = cursor.column_starts_at(); - format!("{rows_seen},{columns_seen}") + let Position { row, column } = cursor.window_origin(); + format!("{row},{column}") } } -fn report_row_position(cursor: XYCursor) -> String { - if cursor.row_starts_at() == 0 { +fn report_row_position(cursor: WindowCursor2D) -> String { + if cursor.window_origin().row == 0 { String::from("Top") } else { let percent_rows = get_percentage(cursor.row(), cursor.row_limit());