e0bd0be3d5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
196 lines
7.0 KiB
Rust
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, ®);
|
|
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, ®);
|
|
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, ®);
|
|
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, ®);
|
|
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, ®);
|
|
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, ®);
|
|
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, ®);
|
|
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![]
|
|
}
|
|
}
|
|
}
|