refactor: split cmd.rs
This commit is contained in:
300
src/command/cmd/grid.rs
Normal file
300
src/command/cmd/grid.rs
Normal file
@ -0,0 +1,300 @@
|
||||
use crate::model::cell::CellValue;
|
||||
use crate::ui::effect::{self, Effect};
|
||||
use crate::view::AxisEntry;
|
||||
|
||||
use super::core::{Cmd, CmdContext};
|
||||
|
||||
// ── Grid operations ─────────────────────────────────────────────────────
|
||||
|
||||
/// Toggle the row or column group collapse under the cursor.
|
||||
#[derive(Debug)]
|
||||
pub struct ToggleGroupAtCursor {
|
||||
pub is_row: bool,
|
||||
}
|
||||
impl Cmd for ToggleGroupAtCursor {
|
||||
fn name(&self) -> &'static str {
|
||||
if self.is_row {
|
||||
"toggle-group-under-cursor"
|
||||
} else {
|
||||
"toggle-col-group-under-cursor"
|
||||
}
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let lookup = if self.is_row {
|
||||
ctx.layout.row_group_for(ctx.selected.0)
|
||||
} else {
|
||||
ctx.layout.col_group_for(ctx.selected.1)
|
||||
};
|
||||
let Some((cat, group)) = lookup else {
|
||||
return vec![];
|
||||
};
|
||||
vec![
|
||||
Box::new(effect::ToggleGroup {
|
||||
category: cat,
|
||||
group,
|
||||
}),
|
||||
effect::mark_dirty(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide the row item at the cursor.
|
||||
#[derive(Debug)]
|
||||
pub struct HideSelectedRowItem;
|
||||
impl Cmd for HideSelectedRowItem {
|
||||
fn name(&self) -> &'static str {
|
||||
"hide-selected-row-item"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let Some(cat_name) = ctx.layout.row_cats.first().cloned() else {
|
||||
return vec![];
|
||||
};
|
||||
let sel_row = ctx.selected.0;
|
||||
let Some(items) = ctx
|
||||
.layout
|
||||
.row_items
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
if let AxisEntry::DataItem(v) = e {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.nth(sel_row)
|
||||
else {
|
||||
return vec![];
|
||||
};
|
||||
let item_name = items[0].clone();
|
||||
vec![
|
||||
Box::new(effect::HideItem {
|
||||
category: cat_name,
|
||||
item: item_name,
|
||||
}),
|
||||
effect::mark_dirty(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigate back or forward in view history.
|
||||
#[derive(Debug)]
|
||||
pub struct ViewNavigate {
|
||||
pub forward: bool,
|
||||
}
|
||||
impl Cmd for ViewNavigate {
|
||||
fn name(&self) -> &'static str {
|
||||
if self.forward {
|
||||
"view-forward"
|
||||
} else {
|
||||
"view-back"
|
||||
}
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
if self.forward {
|
||||
if ctx.view_forward_stack.is_empty() {
|
||||
vec![effect::set_status("No forward view")]
|
||||
} else {
|
||||
vec![Box::new(effect::ViewForward)]
|
||||
}
|
||||
} else {
|
||||
if ctx.view_back_stack.is_empty() {
|
||||
vec![effect::set_status("No previous view")]
|
||||
} else {
|
||||
vec![
|
||||
Box::new(effect::ApplyAndClearDrill),
|
||||
Box::new(effect::ViewBack),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drill down into an aggregated cell: create a _Drill view with _Index on
|
||||
/// Row and _Dim on Column (records/long-format view). Fixed coordinates
|
||||
/// from the drilled cell become page filters.
|
||||
#[derive(Debug)]
|
||||
pub struct DrillIntoCell {
|
||||
pub key: crate::model::cell::CellKey,
|
||||
}
|
||||
impl Cmd for DrillIntoCell {
|
||||
fn name(&self) -> &'static str {
|
||||
"drill-into-cell"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let drill_name = "_Drill".to_string();
|
||||
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
|
||||
|
||||
// Capture the records snapshot NOW (before we switch views).
|
||||
let records: Vec<(crate::model::cell::CellKey, crate::model::cell::CellValue)> =
|
||||
if self.key.0.is_empty() {
|
||||
ctx.model
|
||||
.data
|
||||
.iter_cells()
|
||||
.map(|(k, v)| (k, v.clone()))
|
||||
.collect()
|
||||
} else {
|
||||
ctx.model
|
||||
.data
|
||||
.matching_cells(&self.key.0)
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.clone()))
|
||||
.collect()
|
||||
};
|
||||
let n = records.len();
|
||||
|
||||
// Freeze the snapshot in the drill state
|
||||
effects.push(Box::new(effect::StartDrill(records)));
|
||||
|
||||
// Create (or replace) the drill view
|
||||
effects.push(Box::new(effect::CreateView(drill_name.clone())));
|
||||
effects.push(Box::new(effect::SwitchView(drill_name)));
|
||||
|
||||
// Records mode: _Index on Row, _Dim on Column
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: "_Index".to_string(),
|
||||
axis: crate::view::Axis::Row,
|
||||
}));
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: "_Dim".to_string(),
|
||||
axis: crate::view::Axis::Column,
|
||||
}));
|
||||
|
||||
// Fixed coords (from drilled cell) -> Page with that value as filter
|
||||
let fixed_cats: std::collections::HashSet<String> =
|
||||
self.key.0.iter().map(|(c, _)| c.clone()).collect();
|
||||
for (cat, item) in &self.key.0 {
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: cat.clone(),
|
||||
axis: crate::view::Axis::Page,
|
||||
}));
|
||||
effects.push(Box::new(effect::SetPageSelection {
|
||||
category: cat.clone(),
|
||||
item: item.clone(),
|
||||
}));
|
||||
}
|
||||
// Previously-aggregated categories (none_cats) stay on Axis::None so
|
||||
// they don't filter records; they'll appear as columns in records mode.
|
||||
// Skip virtual categories — we already set _Index/_Dim above.
|
||||
for cat in ctx.none_cats() {
|
||||
if fixed_cats.contains(cat) || cat.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: cat.clone(),
|
||||
axis: crate::view::Axis::None,
|
||||
}));
|
||||
}
|
||||
|
||||
effects.push(effect::set_status(format!("Drilled into cell: {n} rows")));
|
||||
effects
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle pruning of empty rows/columns in the current view.
|
||||
#[derive(Debug)]
|
||||
pub struct TogglePruneEmpty;
|
||||
impl Cmd for TogglePruneEmpty {
|
||||
fn name(&self) -> &'static str {
|
||||
"toggle-prune-empty"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let currently_on = ctx.model.active_view().prune_empty;
|
||||
vec![
|
||||
Box::new(effect::TogglePruneEmpty),
|
||||
effect::set_status(if currently_on {
|
||||
"Showing all rows/columns"
|
||||
} else {
|
||||
"Hiding empty rows/columns"
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle between records mode and pivot mode using the view stack.
|
||||
/// Entering records mode creates a `_Records` view and switches to it.
|
||||
/// Leaving records mode navigates back to the previous view.
|
||||
#[derive(Debug)]
|
||||
pub struct ToggleRecordsMode;
|
||||
impl Cmd for ToggleRecordsMode {
|
||||
fn name(&self) -> &'static str {
|
||||
"toggle-records-mode"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let is_records = ctx.layout.is_records_mode();
|
||||
|
||||
if is_records {
|
||||
// Navigate back to the previous view (restores original axes)
|
||||
return vec![Box::new(effect::ViewBack), effect::set_status("Pivot mode")];
|
||||
}
|
||||
|
||||
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
|
||||
let records_name = "_Records".to_string();
|
||||
|
||||
// Create (or replace) a _Records view and switch to it
|
||||
effects.push(Box::new(effect::CreateView(records_name.clone())));
|
||||
effects.push(Box::new(effect::SwitchView(records_name)));
|
||||
|
||||
// _Index on Row, _Dim on Column, everything else -> None
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: "_Index".to_string(),
|
||||
axis: crate::view::Axis::Row,
|
||||
}));
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: "_Dim".to_string(),
|
||||
axis: crate::view::Axis::Column,
|
||||
}));
|
||||
for name in ctx.model.categories.keys() {
|
||||
if name != "_Index" && name != "_Dim" {
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: name.clone(),
|
||||
axis: crate::view::Axis::None,
|
||||
}));
|
||||
}
|
||||
}
|
||||
effects.push(effect::set_status("Records mode"));
|
||||
effects
|
||||
}
|
||||
}
|
||||
|
||||
/// In records mode, add a new row with an empty value. The new cell gets
|
||||
/// coords from the current page filters. In pivot mode, this is a no-op.
|
||||
#[derive(Debug)]
|
||||
pub struct AddRecordRow;
|
||||
impl Cmd for AddRecordRow {
|
||||
fn name(&self) -> &'static str {
|
||||
"add-record-row"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let is_records = ctx
|
||||
.cell_key()
|
||||
.as_ref()
|
||||
.and_then(crate::view::synthetic_record_info)
|
||||
.is_some();
|
||||
if !is_records {
|
||||
return vec![effect::set_status(
|
||||
"add-record-row only works in records mode",
|
||||
)];
|
||||
}
|
||||
// Build a CellKey from the current page filters
|
||||
let view = ctx.model.active_view();
|
||||
let page_cats: Vec<String> = view
|
||||
.categories_on(crate::view::Axis::Page)
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
let coords: Vec<(String, String)> = page_cats
|
||||
.iter()
|
||||
.map(|cat| {
|
||||
let sel = view.page_selection(cat).unwrap_or("").to_string();
|
||||
(cat.clone(), sel)
|
||||
})
|
||||
.filter(|(_, v)| !v.is_empty())
|
||||
.collect();
|
||||
let key = crate::model::cell::CellKey::new(coords);
|
||||
vec![
|
||||
Box::new(effect::SetCell(key, CellValue::Number(0.0))),
|
||||
effect::mark_dirty(),
|
||||
effect::set_status("Added new record row"),
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user