mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-10 04:19:48 -06:00
Merge origin/main (6f52cc50, the squash of upstream pull request 471) into w/form-controls
Both sides added a kind of floating object (equation, form control); both are kept everywhere. ROADMAP.md: header combined (form controls first, then equations), catalog 281 / 296 as `cargo xtask parity` measures, the form-controls progress-log and revision rows above main's equation rows. crates/engine/src/cmd.rs: both spec lists (controls and equation). crates/engine/src/cmd/extra.rs: the arrange params docs list both kinds. crates/engine/src/cmd/extra/objects.rs: main's `found` match and `anchors_of` on ObjKind gain the Control arms; the unlocked check takes the parsed kind directly. crates/engine/src/cmd/insert.rs: the object params docs and the kind error name both kinds. crates/engine/src/tests.rs: both test modules. crates/engine/src/tests/objects.rs: every_kind returns six objects (equation at L18, slider at P18) and the anchor lists carry both. crates/model/src/features.rs: ObjKind has Equation and Control, ALL lists six kinds, name() both. crates/model/src/sheet.rs: both imports, both accessor arms, the order test inserts equations (6, 8) and controls (13, 15) and documents controls after slicers with equations counted as shapes. crates/model/src/workbook.rs: main's refresh_equations and this branch's free-id doc comment. crates/ui-egui/src/chartview.rs: both doc comments combined. crates/ui-egui/src/objects.rs: both OpenAction variants and arms. crates/ui-egui/src/ribbon/insert.rs: the import list is the union. docs/control-protocol.md: the kind list names equation and control; main's equation sentences and this branch's form-controls sentence. docs/file-format-parity.md: header combined; the xlsx row carries both additions; the form-controls revision row above main's equation rows. docs/parity-checklist.md: regenerated with `cargo xtask parity` (281 / 296). docs/target-app-parity.md: header combined; catalog 281 / 296; the Insert objects row carries both additions; the form-controls revision row (277 / 292 → 281 / 296) above main's. docs/ui-parity.md: header combined with catalog 281 / 296; main's move-and-resize row with equations plus the form-controls row; the form-controls revision row above main's.
This commit is contained in:
@@ -20,8 +20,8 @@ fn id_of(v: &serde_json::Value, key: &str) -> u32 {
|
||||
}
|
||||
|
||||
/// A table with one object of each kind on the first sheet: a chart, a picture, a shape, a
|
||||
/// slicer and a form control, in `ObjKind::ALL` order.
|
||||
fn every_kind(s: &mut Session) -> [ObjRef; 5] {
|
||||
/// slicer, an equation and a form control, in `ObjKind::ALL` order.
|
||||
fn every_kind(s: &mut Session) -> [ObjRef; 6] {
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [["Region", "Qty"], ["East", 3], ["West", 5], ["East", 2], ["North", 8]]})).unwrap();
|
||||
s.execute("insert.table", json!({"range": "A1:B5", "name": "Sales"})).unwrap();
|
||||
let chart = id_of(&s.execute("insert.chart", json!({"range": "B1:B5", "type": "column", "at": "D2"})).unwrap(), "chart");
|
||||
@@ -29,12 +29,14 @@ fn every_kind(s: &mut Session) -> [ObjRef; 5] {
|
||||
let shape = id_of(&s.execute("insert.shape", json!({"kind": "ellipse", "at": "H18"})).unwrap(), "shape");
|
||||
s.execute("selection.set", json!({"cell": "A2"})).unwrap();
|
||||
let slicer = id_of(&s.execute("insert.slicer", json!({"column": "Region", "at": "L2"})).unwrap(), "slicer");
|
||||
let equation = id_of(&s.execute("insert.equation", json!({"latex": "x^2", "cell": "L18"})).unwrap(), "equation");
|
||||
let control = id_of(&s.execute("insert.slider", json!({"at": "P18", "link": "$B$7"})).unwrap(), "control");
|
||||
[
|
||||
ObjRef::new(ObjKind::Chart, chart),
|
||||
ObjRef::new(ObjKind::Image, image),
|
||||
ObjRef::new(ObjKind::Shape, shape),
|
||||
ObjRef::new(ObjKind::Slicer, slicer),
|
||||
ObjRef::new(ObjKind::Equation, equation),
|
||||
ObjRef::new(ObjKind::Control, control),
|
||||
]
|
||||
}
|
||||
@@ -52,6 +54,9 @@ fn every_kind_is_found_moved_and_deleted_by_kind_and_id() {
|
||||
s.execute("object.setAnchorMode", json!({"kind": o.kind.name(), "id": o.id, "mode": "absolute"})).unwrap();
|
||||
assert_eq!(sheet(&s, 0).object_anchor(o).unwrap().mode, AnchorMode::Absolute);
|
||||
}
|
||||
// An equation is a shape too; a plain shape is not an equation.
|
||||
assert!(sheet(&s, 0).has_object(ObjRef::new(ObjKind::Shape, objs[4].id)));
|
||||
assert!(s.execute("object.delete", json!({"kind": "equation", "id": objs[2].id})).is_err());
|
||||
// A kind that does not match the id, an unknown id or kind, and a missing kind are refused.
|
||||
assert!(s.execute("object.delete", json!({"kind": "slicer", "id": objs[0].id})).is_err());
|
||||
assert!(s.execute("object.delete", json!({"kind": "chart", "id": 9999})).is_err());
|
||||
@@ -65,9 +70,9 @@ fn every_kind_is_found_moved_and_deleted_by_kind_and_id() {
|
||||
assert!(sheet(&s, 0).has_object(*later), "deleting {o:?} leaves {later:?}");
|
||||
}
|
||||
}
|
||||
// One undo step per delete brings the control back.
|
||||
// One undo step per delete brings the control, deleted last, back.
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(sheet(&s, 0).has_object(objs[4]));
|
||||
assert!(sheet(&s, 0).has_object(objs[5]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -85,7 +90,7 @@ fn selection_pane_lists_and_stacks_slicers() {
|
||||
assert_eq!(kinds.get(kinds.len() - 2), Some(&"slicer"), "slicers stack above charts, pictures and shapes");
|
||||
let names: Vec<&str> = items.iter().map(|o| o["name"].as_str().unwrap()).collect();
|
||||
assert!(names.contains(&"Region") && names.contains(&"Qty"), "{names:?}");
|
||||
assert!(names.contains(&format!("Slider {}", objs[4].id).as_str()), "{names:?}");
|
||||
assert!(names.contains(&format!("Slider {}", objs[5].id).as_str()), "{names:?}");
|
||||
assert_eq!(sheet(&s, 0).slicers.first().map(|c| c.id), Some(objs[3].id));
|
||||
s.execute("arrange.sendBackward", json!({"kind": "slicer", "id": second, "toBack": true})).unwrap();
|
||||
assert_eq!(sheet(&s, 0).slicers.first().map(|c| c.id), Some(second));
|
||||
@@ -121,17 +126,17 @@ fn cell_of(s: &Session, o: ObjRef) -> String {
|
||||
fn every_kind_follows_row_and_column_edits_and_sorts() {
|
||||
let mut s = s();
|
||||
let objs = every_kind(&mut s);
|
||||
// Anchors: chart D2, picture D18, shape H18, slicer L2, slider P18.
|
||||
// Anchors: chart D2, picture D18, shape H18, slicer L2, equation L18, slider P18.
|
||||
s.execute("object.setAnchorMode", json!({"kind": "shape", "id": objs[2].id, "mode": "absolute"})).unwrap();
|
||||
let cells = |s: &Session| objs.map(|o| cell_of(s, o));
|
||||
assert_eq!(cells(&s), ["D2", "D18", "H18", "L2", "P18"]);
|
||||
assert_eq!(cells(&s), ["D2", "D18", "H18", "L2", "L18", "P18"]);
|
||||
s.execute("home.insertRows", json!({"rows": "10:11"})).unwrap();
|
||||
assert_eq!(cells(&s), ["D2", "D20", "H18", "L2", "P20"], "rows inserted above move the objects below them; an absolute shape stays");
|
||||
assert_eq!(cells(&s), ["D2", "D20", "H18", "L2", "L20", "P20"], "rows inserted above move the objects below them; an absolute shape stays");
|
||||
s.execute("home.insertColumns", json!({"cols": "C:C"})).unwrap();
|
||||
assert_eq!(cells(&s), ["E2", "E20", "H18", "M2", "Q20"]);
|
||||
assert_eq!(cells(&s), ["E2", "E20", "H18", "M2", "M20", "Q20"]);
|
||||
s.execute("home.deleteColumns", json!({"cols": "C:C"})).unwrap();
|
||||
s.execute("home.deleteRows", json!({"rows": "10:11"})).unwrap();
|
||||
assert_eq!(cells(&s), ["D2", "D18", "H18", "L2", "P18"]);
|
||||
assert_eq!(cells(&s), ["D2", "D18", "H18", "L2", "L18", "P18"]);
|
||||
// Sorting moves a picture anchored inside the sorted block with its row.
|
||||
s.execute("range.setValues", json!({"range": "T1", "values": [["k"], [3], [1], [2]]})).unwrap();
|
||||
let pic = ObjRef::new(ObjKind::Image, super::add_image(&mut s, "T2", AnchorMode::MoveAndSize));
|
||||
@@ -269,9 +274,11 @@ fn a_copied_sheet_gives_its_objects_fresh_ids() {
|
||||
ids.dedup();
|
||||
assert_eq!(ids.len(), 2 * objs.len(), "every object id stays unique across the workbook");
|
||||
let copy = sheet(&s, 1);
|
||||
for (o, id) in objs.iter().zip(ids_of(copy)) {
|
||||
assert_eq!(copy.object_ref(id).map(|c| c.kind), Some(o.kind));
|
||||
}
|
||||
let mut kinds: Vec<&str> = ids_of(copy).into_iter().filter_map(|id| copy.object_ref(id)).map(|o| o.kind.name()).collect();
|
||||
let mut want: Vec<&str> = objs.iter().map(|o| o.kind.name()).collect();
|
||||
kinds.sort_unstable();
|
||||
want.sort_unstable();
|
||||
assert_eq!(kinds, want, "the copies keep their kinds");
|
||||
assert!(s.doc().unwrap().wb.next_object_id() > *ids.last().unwrap_or(&0));
|
||||
}
|
||||
|
||||
@@ -389,3 +396,106 @@ fn chart_set_keeps_a_chart_size_a_file_made_larger_than_the_limit() {
|
||||
s.execute("chart.set", json!({"chart": chart, "width": 50_000, "height": 50_000})).unwrap();
|
||||
assert_eq!(size(&s), (11_990.0, 10_000.0), "hostile sizes are capped at the current width and at 10,000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrange_and_align_match_a_shape_id_to_its_kind() {
|
||||
// `arrange.*` and `arrange.align` under `kind: "equation"` act on equations only: an
|
||||
// ordinary shape's id given as an equation is refused by `arrange` and left in place by
|
||||
// `align`, as `object.delete` refuses it. An equation is a shape too, so its id works under
|
||||
// `shape`, as for the other object commands; an unknown kind, and an id of no object of the
|
||||
// kind, are refused instead of acting on whatever shape has the id.
|
||||
let mut s = s();
|
||||
let objs = every_kind(&mut s);
|
||||
let (shape, equation) = (objs[2], objs[4]);
|
||||
let order = |s: &Session| -> Vec<u32> { sheet(s, 0).shapes.iter().map(|c| c.id).collect() };
|
||||
assert_eq!(order(&s), vec![shape.id, equation.id]);
|
||||
assert!(s.execute("arrange.bringForward", json!({"kind": "equation", "id": shape.id, "toFront": true})).is_err());
|
||||
assert_eq!(order(&s), vec![shape.id, equation.id], "the shape is not reordered as an equation");
|
||||
s.execute("arrange.sendBackward", json!({"kind": "equation", "id": equation.id, "toBack": true})).unwrap();
|
||||
assert_eq!(order(&s), vec![equation.id, shape.id]);
|
||||
s.execute("arrange.bringForward", json!({"kind": "shape", "id": equation.id, "toFront": true})).unwrap();
|
||||
assert_eq!(order(&s), vec![shape.id, equation.id], "an equation is a shape too");
|
||||
assert!(s.execute("arrange.bringForward", json!({"kind": "widget", "id": shape.id})).is_err());
|
||||
assert!(s.execute("arrange.bringForward", json!({"kind": "chart", "id": 9999})).is_err());
|
||||
assert!(s.execute("arrange.bringForward", json!({"kind": "shape", "id": objs[0].id})).is_err(), "a chart's id is not a shape");
|
||||
// Align: the shape's id given as an equation is refused and nothing moves; the two equations
|
||||
// line up on their own.
|
||||
let second = ObjRef::new(ObjKind::Equation, id_of(&s.execute("insert.equation", json!({"latex": "y", "cell": "B30"})).unwrap(), "equation"));
|
||||
let before = (anchor_of(&s, shape), anchor_of(&s, equation));
|
||||
assert!(s.execute("arrange.align", json!({"kind": "equation", "ids": [shape.id, equation.id, second.id], "align": "left"})).is_err());
|
||||
assert_eq!((anchor_of(&s, shape), anchor_of(&s, equation)), before, "nothing is aligned with a refused id");
|
||||
s.execute("arrange.align", json!({"kind": "equation", "ids": [equation.id, second.id], "align": "left"})).unwrap();
|
||||
let (a, b) = (sheet(&s, 0).object_anchor(equation).unwrap(), sheet(&s, 0).object_anchor(second).unwrap());
|
||||
assert_eq!((a.cell.col, a.dx), (b.cell.col, b.dx), "the equations line up");
|
||||
assert!(s.execute("arrange.align", json!({"kind": "widget", "ids": [shape.id, equation.id], "align": "left"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrange_refuses_a_kind_that_is_present_but_not_a_name() {
|
||||
// `kind` is `shape` only when the parameter is absent: `null`, a number or another non-name
|
||||
// is refused, so a shape's id is not reordered or aligned under it. The `object.*` commands
|
||||
// refuse the same values, their kind being required.
|
||||
let mut s = s();
|
||||
let objs = every_kind(&mut s);
|
||||
let (shape, equation) = (objs[2], objs[4]);
|
||||
let order = |s: &Session| -> Vec<u32> { sheet(s, 0).shapes.iter().map(|c| c.id).collect() };
|
||||
let before = (order(&s), anchor_of(&s, shape));
|
||||
for kind in [json!(null), json!(7), json!(["shape"]), json!({"kind": "shape"}), json!(true)] {
|
||||
assert!(s.execute("arrange.bringForward", json!({"kind": kind, "id": shape.id, "toFront": true})).is_err(), "{kind}");
|
||||
assert!(s.execute("arrange.align", json!({"kind": kind, "ids": [shape.id, equation.id], "align": "left"})).is_err(), "{kind}");
|
||||
assert!(s.execute("object.move", json!({"kind": kind, "id": shape.id, "at": "A1"})).is_err(), "{kind}");
|
||||
}
|
||||
assert_eq!((order(&s), anchor_of(&s, shape)), before, "nothing moved");
|
||||
// Absent, the kind is `shape`, as before.
|
||||
s.execute("arrange.bringForward", json!({"id": shape.id, "toFront": true})).unwrap();
|
||||
assert_eq!(order(&s).last(), Some(&shape.id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn align_refuses_an_id_that_is_not_an_object_of_the_kind() {
|
||||
// `arrange.align` and `arrange.group` refuse an id in `ids` that is not an object of the
|
||||
// kind, or not a number, as `arrange` and the `object.*` commands refuse it, instead of
|
||||
// aligning the rest and reporting success; nothing moves. With fewer than two ids they
|
||||
// change nothing and succeed.
|
||||
let mut s = s();
|
||||
let objs = every_kind(&mut s);
|
||||
let (shape, equation) = (objs[2], objs[4]);
|
||||
let second = ObjRef::new(ObjKind::Equation, id_of(&s.execute("insert.equation", json!({"latex": "y", "cell": "B30"})).unwrap(), "equation"));
|
||||
let before = (anchor_of(&s, shape), anchor_of(&s, equation), anchor_of(&s, second));
|
||||
for ids in [
|
||||
json!([shape.id, equation.id, second.id]),
|
||||
json!([equation.id, second.id, 9999]),
|
||||
json!([equation.id, second.id, -1]),
|
||||
json!([equation.id, second.id, "x"]),
|
||||
json!([equation.id, second.id, 1e12]),
|
||||
json!([equation.id, second.id, null]),
|
||||
] {
|
||||
assert!(s.execute("arrange.align", json!({"kind": "equation", "ids": ids, "align": "left"})).is_err(), "{ids}");
|
||||
assert!(s.execute("arrange.group", json!({"kind": "equation", "ids": ids})).is_err(), "{ids}");
|
||||
}
|
||||
assert_eq!((anchor_of(&s, shape), anchor_of(&s, equation), anchor_of(&s, second)), before, "nothing moved");
|
||||
s.execute("arrange.align", json!({"kind": "equation", "ids": [equation.id], "align": "left"})).unwrap();
|
||||
s.execute("arrange.align", json!({"kind": "equation", "align": "left"})).unwrap();
|
||||
assert_eq!(anchor_of(&s, equation), before.1, "fewer than two ids change nothing");
|
||||
s.execute("arrange.align", json!({"kind": "equation", "ids": [equation.id, second.id], "align": "left"})).unwrap();
|
||||
let (a, b) = (sheet(&s, 0).object_anchor(equation).unwrap(), sheet(&s, 0).object_anchor(second).unwrap());
|
||||
assert_eq!((a.cell.col, a.dx), (b.cell.col, b.dx), "the equations line up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn align_refuses_ids_that_are_not_an_array() {
|
||||
// `ids` may be absent (nothing to align) but, present, must be an array of object ids: `null`,
|
||||
// a number, a string or an object is refused by `arrange.align` and `arrange.group`, and
|
||||
// nothing moves.
|
||||
let mut s = s();
|
||||
let objs = every_kind(&mut s);
|
||||
let (shape, equation) = (objs[2], objs[4]);
|
||||
let before = (anchor_of(&s, shape), anchor_of(&s, equation));
|
||||
for ids in [json!(null), json!(7), json!("x"), json!({"ids": [1]})] {
|
||||
assert!(s.execute("arrange.align", json!({"kind": "shape", "ids": ids, "align": "left"})).is_err(), "{ids}");
|
||||
assert!(s.execute("arrange.group", json!({"kind": "shape", "ids": ids})).is_err(), "{ids}");
|
||||
}
|
||||
s.execute("arrange.align", json!({"kind": "shape", "align": "left"})).unwrap();
|
||||
s.execute("arrange.group", json!({"kind": "shape"})).unwrap();
|
||||
assert_eq!((anchor_of(&s, shape), anchor_of(&s, equation)), before, "nothing moved");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user