diff --git a/src/ui/app.rs b/src/ui/app.rs index 9f81263..f819dd3 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -283,6 +283,11 @@ impl App { } } + // ── Grid transpose ───────────────────────────────────────────── + (KeyCode::Char('t'), KeyModifiers::NONE) => { + self.model.active_view_mut().transpose_axes(); + } + // ── Tile movement ────────────────────────────────────────────── // T = enter tile select mode (single key, no Ctrl needed) (KeyCode::Char('T'), _) => { @@ -1135,7 +1140,7 @@ impl App { /// Hint text for the status bar (context-sensitive) pub fn hint_text(&self) -> &'static str { match &self.mode { - AppMode::Normal => "hjkl:nav Enter:advance i:edit x:clear /:search F/C/V:panels T:tiles [:]:page ::cmd", + AppMode::Normal => "hjkl:nav Enter:advance i:edit x:clear t:transpose /:search F/C/V:panels T:tiles [:]:page ::cmd", AppMode::Editing { .. } => "Enter:commit Esc:cancel", AppMode::FormulaPanel => "n:new d:delete jk:nav Esc:back", AppMode::FormulaEdit { .. } => "Enter:save Esc:cancel — type: Name = expression", diff --git a/src/view/view.rs b/src/view/view.rs index 5867451..5bbdfdb 100644 --- a/src/view/view.rs +++ b/src/view/view.rs @@ -112,6 +112,18 @@ impl View { self.hidden_items.get(cat_name).map(|s| s.contains(item_name)).unwrap_or(false) } + /// Swap all Row categories to Column and all Column categories to Row. + /// Page categories are unaffected. + pub fn transpose_axes(&mut self) { + let rows: Vec = self.categories_on(Axis::Row).iter().map(|s| s.to_string()).collect(); + let cols: Vec = self.categories_on(Axis::Column).iter().map(|s| s.to_string()).collect(); + for cat in &rows { self.set_axis(cat, Axis::Column); } + for cat in &cols { self.set_axis(cat, Axis::Row); } + self.selected = (0, 0); + self.row_offset = 0; + self.col_offset = 0; + } + /// Cycle axis for a category: Row → Column → Page → Row pub fn cycle_axis(&mut self, cat_name: &str) { let next = match self.axis_of(cat_name) { @@ -171,6 +183,34 @@ mod tests { assert_eq!(v.categories_on(Axis::Page), vec!["Time"]); } + #[test] + fn transpose_axes_swaps_row_and_column() { + let mut v = view_with_cats(&["Region", "Product"]); + // Default: Region=Row, Product=Column + assert_eq!(v.axis_of("Region"), Axis::Row); + assert_eq!(v.axis_of("Product"), Axis::Column); + v.transpose_axes(); + assert_eq!(v.axis_of("Region"), Axis::Column); + assert_eq!(v.axis_of("Product"), Axis::Row); + } + + #[test] + fn transpose_axes_leaves_page_categories_unchanged() { + let mut v = view_with_cats(&["Region", "Product", "Time"]); + // Default: Region=Row, Product=Column, Time=Page + v.transpose_axes(); + assert_eq!(v.axis_of("Time"), Axis::Page); + } + + #[test] + fn transpose_axes_is_its_own_inverse() { + let mut v = view_with_cats(&["Region", "Product"]); + v.transpose_axes(); + v.transpose_axes(); + assert_eq!(v.axis_of("Region"), Axis::Row); + assert_eq!(v.axis_of("Product"), Axis::Column); + } + #[test] #[should_panic(expected = "axis_of called for category not registered")] fn axis_of_unknown_category_panics() {