Files
improvise/crates/improvise-command/src/command/cmd/tile.rs
T

196 lines
7.0 KiB
Rust

use crate::ui::effect::{self, Effect, ViewTarget};
use crate::view::Axis;
use super::core::{Cmd, CmdContext};
#[cfg(test)]
mod tests {
use super::*;
use crate::command::cmd::test_helpers::*;
#[test]
fn a_tile_status_names_the_display_not_the_key() {
let mut m = two_cat_model();
m.model
.category_mut("Type")
.unwrap()
.set_display("Kind of food and drink");
let layout = make_layout(&m);
let reg = make_registry();
let mut ctx = make_ctx(&m, &layout, &reg);
ctx.tile_cat_idx = ctx
.view
.tileable_cats(&ctx.model.category_names())
.iter()
.position(|c| *c == "Type")
.unwrap();
let effects = TileAxisOp { axis: None }.execute(&ctx);
let said = format!("{:?}", effects.last().unwrap());
assert!(said.contains("Kind of food and drink → "), "{said}");
assert!(!said.contains("Type →"), "the key was said: {said}");
}
#[test]
fn tile_axis_cycle_produces_cycle_effect() {
let m = two_cat_model();
let layout = make_layout(&m);
let reg = make_registry();
let ctx = make_ctx(&m, &layout, &reg);
let cmd = TileAxisOp { axis: None };
let effects = cmd.execute(&ctx);
assert!(!effects.is_empty());
// The tile cursor indexes EVERY tileable category, virtual ones included,
// so cursor 0 is `_Index` rather than the first regular category.
let before = m.active_view().axis_of("_Index");
let mut app = crate::ui::app::App::new(two_cat_model(), None);
app.apply_effects(effects);
let view = app.model_state.workbook.active_view();
assert_eq!(view.axis_of("_Index"), before.next());
assert_eq!(view.axis_of("Type"), m.active_view().axis_of("Type"));
assert_eq!(view.axis_of("Month"), m.active_view().axis_of("Month"));
}
#[test]
fn tile_axis_set_produces_set_axis_effect() {
let m = two_cat_model();
let layout = make_layout(&m);
let reg = make_registry();
let ctx = make_ctx(&m, &layout, &reg);
let cmd = TileAxisOp {
axis: Some(crate::view::Axis::Page),
};
let effects = cmd.execute(&ctx);
assert!(!effects.is_empty());
let mut app = crate::ui::app::App::new(two_cat_model(), None);
app.apply_effects(effects);
let view = app.model_state.workbook.active_view();
assert_eq!(view.axis_of("_Index"), Axis::Page);
assert_eq!(view.axis_of("Type"), m.active_view().axis_of("Type"));
assert_eq!(view.axis_of("Month"), m.active_view().axis_of("Month"));
}
#[test]
fn tile_axis_with_out_of_bounds_cursor_returns_empty() {
let m = two_cat_model();
let layout = make_layout(&m);
let reg = make_registry();
let mut ctx = make_ctx(&m, &layout, &reg);
ctx.tile_cat_idx = 999;
let cmd = TileAxisOp { axis: None };
let effects = cmd.execute(&ctx);
assert!(effects.is_empty());
}
/// improvise-jgzw: `_Index`/`_Dim` stay tileable on purpose, and `_Measure` on
/// a column must too — `context/test-evidence/command.md`.
#[test]
fn the_tile_bar_offers_every_category_by_name_measure_and_records_axes_included() {
let mut m = two_cat_model();
m.active_view_mut().set_axis("_Measure", Axis::Column);
let layout = make_layout(&m);
let reg = make_registry();
let mut ctx = make_ctx(&m, &layout, &reg);
let mut offered = Vec::new();
for idx in 0..8 {
ctx.tile_cat_idx = idx;
let dbg = effects_debug(&TileAxisOp { axis: None }.execute(&ctx));
if let Some(rest) = dbg.split("CycleAxis(Active, \"").nth(1) {
offered.push(rest.split('"').next().unwrap_or_default().to_string());
}
}
assert_eq!(offered, ["_Index", "_Dim", "_Measure", "Type", "Month"]);
}
#[test]
fn move_tile_cursor_right() {
let m = two_cat_model();
let layout = make_layout(&m);
let reg = make_registry();
let ctx = make_ctx(&m, &layout, &reg);
let cmd = MoveTileCursor(1);
let effects = cmd.execute(&ctx);
assert_eq!(effects.len(), 1);
let dbg = effects_debug(&effects);
assert!(
dbg.contains("SetTileCatIdx(1)"),
"Expected idx 1, got: {dbg}"
);
}
#[test]
fn move_tile_cursor_clamps_at_start() {
let m = two_cat_model();
let layout = make_layout(&m);
let reg = make_registry();
let ctx = make_ctx(&m, &layout, &reg);
let cmd = MoveTileCursor(-1);
let effects = cmd.execute(&ctx);
assert_eq!(effects.len(), 1);
let dbg = effects_debug(&effects);
assert!(
dbg.contains("SetTileCatIdx(0)"),
"Expected clamped to 0, got: {dbg}"
);
}
}
// ── Tile select commands ────────────────────────────────────────────────────
/// Move the tile select cursor left or right.
#[derive(Debug)]
pub struct MoveTileCursor(pub i32);
impl Cmd for MoveTileCursor {
fn name(&self) -> &'static str {
"move-tile-cursor"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let count = ctx.view.tileable_cats(&ctx.model.category_names()).len();
if count == 0 {
return vec![];
}
let new = (ctx.tile_cat_idx as i32 + self.0).clamp(0, (count - 1) as i32) as usize;
vec![Box::new(effect::SetTileCatIdx(new))]
}
}
/// Cycle or set the axis for the category at the tile cursor, staying in TileSelect.
/// `axis: None` cycles; `axis: Some(a)` sets.
#[derive(Debug)]
pub struct TileAxisOp {
pub axis: Option<Axis>,
}
impl Cmd for TileAxisOp {
fn name(&self) -> &'static str {
if self.axis.is_some() {
"set-axis-for-tile"
} else {
"cycle-axis-for-tile"
}
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let cat_names = ctx.view.tileable_cats(&ctx.model.category_names());
if let Some(name) = cat_names.get(ctx.tile_cat_idx) {
let new_axis = match self.axis {
Some(axis) => axis,
None => ctx.view.axis_of(name).next(),
};
let axis_effect: Box<dyn Effect> = match self.axis {
Some(axis) => Box::new(effect::SetAxis {
view: ViewTarget::Active,
category: name.to_string(),
axis,
}),
None => Box::new(effect::CycleAxis(ViewTarget::Active, name.to_string())),
};
let status = improvise_protocol::viewmodel::axis_change_status(
ctx.model.category_display(name),
new_axis,
);
vec![axis_effect, effect::set_status(status)]
} else {
vec![]
}
}
}