feat(panel): the authoritative cell-mapping key (G1-G4)
The key itself, its domain, and the witnesses for both. No gating and no replay yet. **DERIVED FROM A FINGERPRINT, NOT BUMPED AT MUTATION SITES.** `panel_mapping_fingerprint` hashes what actually decides which byte a cell means --- buffer identity, grid rows and columns, `view_top`, `view_left`, wrap mode, content columns, fold POLICY and fold CONTENT, and the buffer's content revision --- and the generation advances whenever that changes. This makes the changing/stable split STRUCTURAL: an input that is hashed moves the key by construction, and one that is not cannot. Bumping by hand at each mutation site would have made "advances after any mapping mutation" a promise about someone remembering. Folds are hashed at their SOURCE, the registry's ranges, rather than through the derived `VisibleLineMap`, whose only public summary is `is_identity()` --- too coarse, since a fold edit that leaves the map non-identity still changes which source line a row shows. **ONE SEAM, READ BY BOTH SIDES.** `panel_mapping_generation` advances if the fingerprint changed and returns the current value; projection will stamp with it and inbound validation will compare against it, so "what the frontend was shown" and "what the daemon checks" cannot drift. Computed ON DEMAND, deliberately: a mutation not yet painted has still changed the inverse, and a gesture arriving in that gap must be refused. Deriving from the last emitted frame recreates the hole. **Nondecreasing, and never cleared.** `Absent` yields no key to stamp --- which is not a key of zero --- but the high-water mark survives, so a frame delayed across a hide cannot roll authority backward. First establishment takes 1; zero is the wire's invalid value. **MUTATION TESTING FOUND MY WITNESSES UNDER-SPECIFIED, TWICE.** With only the content-edit row present, dropping `view_left` from the key stayed GREEN, and so did collapsing the grid to `rows * cols`. That is exactly what the closure predicted --- "a key that ignores `view_left` passes every row that only scrolls vertically" --- and it is why G2 is enumerated per input rather than asserted in aggregate. Six legs now, one per input, each touching only its own. Mutations that bite: omit the content revision (3 rows), omit `view_left`, omit wrap, omit fold policy, and include the CURSOR --- a stable input, caught by G3. **One mutation does NOT bite, and the row says so rather than pretending.** Collapsing rows and columns to a product stays green, because `last_content_cols` co-varies with a column change and the row count co-varies with a resize: the key moves by another input either way. Only a transposition (2x6 -> 6x2, identical product) would isolate it, and no production path reaches one --- rows come from the band's height, columns from the frame declaration, and nothing swaps them. The key hashes them separately anyway; hashing a product because no test can currently tell the difference would be choosing the weaker construction for the suite's convenience. G4b --- that an in-flight drag survives a selection repaint through real replay --- stays owed by the rebased replay lane, which is the only branch where replay exists. Verified: focused suite 34/34, `cargo test --lib` green, clippy clean. Per the standing procedure the eleven-stage `--protocol` gate is reserved for the next coherent checkpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
758b985c35
commit
063db52555
|
|
@ -2452,6 +2452,80 @@ impl EditorState {
|
|||
true
|
||||
}
|
||||
|
||||
/// Fingerprint of the panel's **inverse mapping** for one frontend
|
||||
/// (§5b, Q#BP-R3).
|
||||
///
|
||||
/// This is the whole of "what decides which byte a cell means". A
|
||||
/// generation is derived from it by advancing whenever it changes,
|
||||
/// which makes the changing/stable split **structural** rather than
|
||||
/// a list of bump sites someone must remember to touch: an input
|
||||
/// that is hashed moves the key by construction, and one that is not
|
||||
/// cannot.
|
||||
///
|
||||
/// **Deliberately EXCLUDED**, and each exclusion is a contract:
|
||||
/// focus, styling and theme, the cursor, and the selection. None of
|
||||
/// them changes which byte a cell denotes, and a drag provokes
|
||||
/// selection repaints on every motion — a key that moved with them
|
||||
/// would cancel the gesture it is meant to protect after one step.
|
||||
///
|
||||
/// Returns `None` when there is no presentable panel, which is not
|
||||
/// the same as a zero key: absence of a mapping is not a mapping.
|
||||
pub fn panel_mapping_fingerprint(&self, frontend_id: FrontendId) -> Option<u64> {
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let core = self.core.borrow();
|
||||
let size = core.panel_grid_size(frontend_id)?;
|
||||
let side = core.side_window_for(frontend_id)?;
|
||||
let window = core.windows.get(&side)?;
|
||||
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
// Identity first: a different buffer is a different mapping even
|
||||
// if every geometry input coincides.
|
||||
window.buffer_id.hash(&mut hasher);
|
||||
// Grid ROWS and COLUMNS hashed separately, not as an area — a
|
||||
// 2x6 and a 6x2 panel invert differently.
|
||||
size.rows.hash(&mut hasher);
|
||||
size.cols.hash(&mut hasher);
|
||||
// Viewport, both axes. `view_left` matters from GUI arc 1b
|
||||
// onward, and is hashed now so the key does not need revisiting
|
||||
// when horizontal scrolling starts moving it.
|
||||
window.view_top.hash(&mut hasher);
|
||||
window.view_left.hash(&mut hasher);
|
||||
// Wrap mode and the content width that the gutter reservation
|
||||
// has already been subtracted from: together these decide how a
|
||||
// source line is broken into display rows and where column zero
|
||||
// sits.
|
||||
window.last_wrap.hash(&mut hasher);
|
||||
window.last_content_cols.hash(&mut hasher);
|
||||
// Fold POLICY and fold CONTENT are separate inputs. The policy
|
||||
// belongs to the owning frontend's view; the content belongs to
|
||||
// the window. Either alone can change which source line a grid
|
||||
// row shows.
|
||||
core.views
|
||||
.get(&frontend_id)
|
||||
.is_some_and(|view| view.fold_projection)
|
||||
.hash(&mut hasher);
|
||||
// Hashed at their SOURCE — the registry's ranges — rather than
|
||||
// through the derived `VisibleLineMap`, whose only public
|
||||
// summary is `is_identity()`. That would be too coarse: a fold
|
||||
// edit that leaves the map non-identity would not move the key
|
||||
// while plainly changing which source line a row shows.
|
||||
for range in core.fold_registry.folds(window.buffer_id) {
|
||||
range.start.hash(&mut hasher);
|
||||
range.end.hash(&mut hasher);
|
||||
}
|
||||
// Content. A foreign edit moves the mapping with every geometry
|
||||
// input untouched, and is the case the epoch ladder cannot see.
|
||||
let registry = core.registry.clone();
|
||||
let revision = registry
|
||||
.borrow()
|
||||
.get(window.buffer_id)
|
||||
.ok()
|
||||
.map(crate::buffer::Buffer::revision);
|
||||
revision.hash(&mut hasher);
|
||||
Some(hasher.finish())
|
||||
}
|
||||
|
||||
/// Paint one semantic frontend's side window into a panel-sized grid
|
||||
/// (Q#BP8, Q#BP15, Q#BP15a, Q#BP17).
|
||||
///
|
||||
|
|
|
|||
|
|
@ -377,6 +377,26 @@ pub struct SemanticRenderState {
|
|||
/// retains its last valid frame and silence would leave a stale band
|
||||
/// on screen indefinitely (Q#BP15).
|
||||
last_panel_payload: Option<PanelFramePayload>,
|
||||
/// §5b — the authoritative **cell-mapping key** for this frontend.
|
||||
///
|
||||
/// `(fingerprint, generation)`. The generation advances whenever the
|
||||
/// fingerprint changes, and **both projection and inbound
|
||||
/// validation read it through the same accessor**, so "what the
|
||||
/// frontend was shown" and "what the daemon checks" cannot drift.
|
||||
///
|
||||
/// It is deliberately **not** recomputed from the last emitted
|
||||
/// frame: a mapping mutation that has not yet been painted has still
|
||||
/// changed the inverse, and a gesture arriving in that gap must be
|
||||
/// refused. Advancing on demand at both seams is what makes
|
||||
/// "advances before the next inbound pointer, whether or not
|
||||
/// anything rendered" true rather than aspirational.
|
||||
///
|
||||
/// **Nondecreasing, and never cleared** — not even by `Absent`. A
|
||||
/// delayed lower frame must not roll the producer's authority
|
||||
/// backward, so this is a high-water mark for the session.
|
||||
/// `generation` starts at 0 meaning "never established"; the first
|
||||
/// real mapping takes 1, because zero is invalid on the wire.
|
||||
panel_mapping: Option<(u64, u64)>,
|
||||
/// Highest presentation epoch allocated for this session; `0` means
|
||||
/// none has been. Advanced only when a frame is actually shipped, so
|
||||
/// a frame that fails validation does not burn an identity the peer
|
||||
|
|
@ -575,6 +595,7 @@ impl SemanticRenderState {
|
|||
// peer already holds. Seeding the baseline keeps the first
|
||||
// frame from shipping a redundant authoritative `Absent`.
|
||||
last_panel_payload: Some(PanelFramePayload::Absent),
|
||||
panel_mapping: None,
|
||||
panel_epoch_used: 0,
|
||||
panel_presentation: None,
|
||||
panel_error_latched: false,
|
||||
|
|
@ -599,6 +620,37 @@ impl SemanticRenderState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Advance-if-changed, then read: the authoritative mapping key.
|
||||
///
|
||||
/// **The single seam §5b requires.** Projection stamps the frame
|
||||
/// with what this returns, and inbound validation compares against
|
||||
/// what this returns; there is no second derivation to disagree
|
||||
/// with.
|
||||
///
|
||||
/// `fingerprint` is `None` when no panel is presentable. That does
|
||||
/// **not** reset the key — the high-water mark survives `Absent`,
|
||||
/// so a frame delayed across a hide cannot come back with a lower
|
||||
/// generation and be believed.
|
||||
pub fn panel_mapping_generation(&mut self, fingerprint: Option<u64>) -> Option<u64> {
|
||||
let fingerprint = fingerprint?;
|
||||
let next = match self.panel_mapping {
|
||||
Some((seen, generation)) if seen == fingerprint => generation,
|
||||
Some((_, generation)) => generation.saturating_add(1),
|
||||
// First establishment takes 1, never 0: zero is the wire's
|
||||
// "uninitialised" value and is refused on sight.
|
||||
None => 1,
|
||||
};
|
||||
self.panel_mapping = Some((fingerprint, next));
|
||||
Some(next)
|
||||
}
|
||||
|
||||
/// The current key without advancing it, for assertions and for
|
||||
/// callers that must not have a side effect.
|
||||
#[must_use]
|
||||
pub fn panel_mapping_generation_peek(&self) -> Option<u64> {
|
||||
self.panel_mapping.map(|(_, generation)| generation)
|
||||
}
|
||||
|
||||
/// Whether the last shipped declaration is a `Present` whose epochs
|
||||
/// both match an inbound panel event **and** which still describes
|
||||
/// the side window that is live now (Q#BP16 steps 2–4).
|
||||
|
|
|
|||
|
|
@ -1362,3 +1362,316 @@ fn sweep_a_panel_wider_than_the_terminal_cap_still_presents_its_terminal() {
|
|||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §5b G1–G4 — the authoritative cell-mapping key
|
||||
//
|
||||
// The key is derived from a FINGERPRINT of the inverse mapping's inputs,
|
||||
// so the changing/stable split is structural: an input that is hashed
|
||||
// moves the key by construction, and one that is not cannot. These rows
|
||||
// pin each input individually, because a single "it changed" row cannot
|
||||
// show WHICH input moved it — and a key that silently ignored, say,
|
||||
// `view_left` would pass every row that only scrolls vertically.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Edit the panel's buffer from OUTSIDE any gesture — the "foreign
|
||||
/// edit" the ladder cannot see. Done in Rust rather than Lua because it
|
||||
/// must be a plain content mutation with no view, cursor or command
|
||||
/// state attached to it.
|
||||
fn foreign_edit(session: &Session, text: &str) {
|
||||
let core = session.state.core.borrow();
|
||||
let side = core.side_window_for(FID).expect("a side window");
|
||||
let buffer_id = core.windows[&side].buffer_id;
|
||||
let registry = core.registry.clone();
|
||||
let mut reg = registry.borrow_mut();
|
||||
let buffer = reg.get_mut(buffer_id).expect("the panel's buffer");
|
||||
buffer
|
||||
.set_generated_contents(text.as_bytes())
|
||||
.expect("a generated-contents write is a plain content change");
|
||||
}
|
||||
|
||||
/// The key as the daemon would compute it for `FID`, advancing on change.
|
||||
fn mapping_generation(session: &mut Session) -> Option<u64> {
|
||||
let fingerprint = session.state.panel_mapping_fingerprint(FID);
|
||||
session.render.panel_mapping_generation(fingerprint)
|
||||
}
|
||||
|
||||
/// §5b G1 — a **foreign** edit before the next render moves the key.
|
||||
///
|
||||
/// This is the case the whole slice exists for: the epoch ladder cannot
|
||||
/// see it. No buffer is replaced, no panel reopens, no geometry is
|
||||
/// re-declared — every epoch holds — and yet the byte under a cell has
|
||||
/// changed. It is also why the key must be derived on demand rather than
|
||||
/// from the last emitted frame: nothing has rendered here.
|
||||
#[test]
|
||||
fn g1_a_foreign_edit_moves_the_mapping_key_before_anything_renders() {
|
||||
let mut session = Session::new();
|
||||
open_panel(&session, "g1", 4);
|
||||
session.declare(1, 24, 80);
|
||||
let _ = session.present();
|
||||
|
||||
let before = mapping_generation(&mut session).expect("a presentable panel has a key");
|
||||
assert!(
|
||||
before >= 1,
|
||||
"a live key is never zero — zero is the wire's invalid value"
|
||||
);
|
||||
|
||||
// An edit from somewhere other than the gesture's frontend, with no
|
||||
// render in between.
|
||||
foreign_edit(&session, "foreign edit\n");
|
||||
|
||||
let after = mapping_generation(&mut session).expect("still presentable");
|
||||
assert!(
|
||||
after > before,
|
||||
"a foreign edit changes which byte a cell means, and no epoch \
|
||||
moves with it — this is the hole the ladder cannot close"
|
||||
);
|
||||
}
|
||||
|
||||
/// §5b G3 — the **stable** inputs, one row each.
|
||||
///
|
||||
/// Every entry here is something that repaints a panel without changing
|
||||
/// which byte a cell denotes. A drag provokes selection repaints on every
|
||||
/// motion, so a key that moved with them would cancel the gesture it
|
||||
/// exists to protect after a single step.
|
||||
#[test]
|
||||
fn g3_repaints_that_cannot_move_a_byte_leave_the_key_alone() {
|
||||
let mut session = Session::new();
|
||||
open_panel(&session, "g3", 4);
|
||||
session.declare(1, 24, 80);
|
||||
let _ = session.present();
|
||||
|
||||
let baseline = mapping_generation(&mut session).expect("a key");
|
||||
|
||||
// Re-reading with nothing changed at all.
|
||||
assert_eq!(
|
||||
mapping_generation(&mut session),
|
||||
Some(baseline),
|
||||
"an idle re-read must not advance the key, or every frame would \
|
||||
cancel every gesture"
|
||||
);
|
||||
|
||||
// Cursor motion the follow rules absorb: the caret moves inside the
|
||||
// viewport, so no origin moves with it. Set directly, so nothing but
|
||||
// the cursor changes.
|
||||
{
|
||||
let mut core = session.state.core.borrow_mut();
|
||||
let side = core.side_window_for(FID).expect("a side window");
|
||||
let window = core.windows.get_mut(&side).expect("the side window");
|
||||
window.cursor = 0;
|
||||
}
|
||||
assert_eq!(
|
||||
mapping_generation(&mut session),
|
||||
Some(baseline),
|
||||
"cursor motion that moves no origin is not a mapping change"
|
||||
);
|
||||
}
|
||||
|
||||
/// §5b G4a — a **selection-only** repaint preserves the key.
|
||||
///
|
||||
/// Split from G3 because it is the one the lifecycle depends on: G4b —
|
||||
/// that an in-flight drag then continues through real replay — is owed by
|
||||
/// the rebased replay lane, which is the only branch where replay exists.
|
||||
#[test]
|
||||
fn g4a_a_selection_only_repaint_preserves_the_mapping_key() {
|
||||
let mut session = Session::new();
|
||||
open_panel(&session, "g4a", 4);
|
||||
session.declare(1, 24, 80);
|
||||
let _ = session.present();
|
||||
|
||||
let baseline = mapping_generation(&mut session).expect("a key");
|
||||
foreign_edit(&session, "alpha beta\n");
|
||||
let after_edit = mapping_generation(&mut session).expect("a key");
|
||||
assert!(after_edit > baseline, "the edit itself is a mapping change");
|
||||
|
||||
// Now a selection, with no content or viewport change.
|
||||
{
|
||||
let mut core = session.state.core.borrow_mut();
|
||||
let side = core.side_window_for(FID).expect("a side window");
|
||||
let window = core.windows.get_mut(&side).expect("the side window");
|
||||
window.selection = Some(pmacs::window::Selection { anchor: 0 });
|
||||
window.cursor = 5;
|
||||
}
|
||||
assert_eq!(
|
||||
mapping_generation(&mut session),
|
||||
Some(after_edit),
|
||||
"a selection changes what is HIGHLIGHTED, never what a cell \
|
||||
denotes — and a drag repaints the selection on every motion"
|
||||
);
|
||||
}
|
||||
|
||||
/// §5b — the key is a **high-water mark** and survives `Absent`.
|
||||
///
|
||||
/// Hiding the band clears input authority, but it must not reset the
|
||||
/// generation: a frame delayed across the hide would otherwise return
|
||||
/// with a lower value and be believed.
|
||||
#[test]
|
||||
fn the_mapping_key_never_moves_backward_across_a_hidden_panel() {
|
||||
let mut session = Session::new();
|
||||
open_panel(&session, "hw", 4);
|
||||
session.declare(1, 24, 80);
|
||||
let _ = session.present();
|
||||
|
||||
let before = mapping_generation(&mut session).expect("a key");
|
||||
foreign_edit(&session, "one\n");
|
||||
let peak = mapping_generation(&mut session).expect("a key");
|
||||
assert!(peak > before);
|
||||
|
||||
// Hide it: no fingerprint, so no advance — and no reset either.
|
||||
{
|
||||
let mut core = session.state.core.borrow_mut();
|
||||
core.views
|
||||
.get_mut(&FID)
|
||||
.expect("the frontend's view")
|
||||
.panel_hidden = true;
|
||||
}
|
||||
assert_eq!(
|
||||
mapping_generation(&mut session),
|
||||
None,
|
||||
"no presentable panel means no key to stamp, which is not the \
|
||||
same as a key of zero"
|
||||
);
|
||||
assert_eq!(
|
||||
session.render.panel_mapping_generation_peek(),
|
||||
Some(peak),
|
||||
"the high-water mark SURVIVES the hide — clearing it would let a \
|
||||
delayed frame roll the producer's authority backward"
|
||||
);
|
||||
}
|
||||
|
||||
/// §5b G2 — **every changing input, one leg each.**
|
||||
///
|
||||
/// Enumerated rather than asserted in aggregate, and mutation testing is
|
||||
/// what forced it: with only the content-edit row present, dropping
|
||||
/// `view_left` from the key and collapsing the grid to `rows * cols`
|
||||
/// both stayed GREEN. A single "the key moved" row cannot show *which*
|
||||
/// input moved it, and a key that ignores horizontal scrolling passes
|
||||
/// every row that only scrolls vertically.
|
||||
#[test]
|
||||
fn g2_each_input_of_the_inverse_mapping_moves_the_key_on_its_own() {
|
||||
// Each leg names one input and touches only that input.
|
||||
type Leg = (&'static str, fn(&Session));
|
||||
|
||||
let legs: &[Leg] = &[
|
||||
("view_top", |session| {
|
||||
let mut core = session.state.core.borrow_mut();
|
||||
let side = core.side_window_for(FID).expect("side");
|
||||
core.windows.get_mut(&side).expect("win").view_top += 1;
|
||||
}),
|
||||
("view_left — GUI arc 1b makes this real", |session| {
|
||||
let mut core = session.state.core.borrow_mut();
|
||||
let side = core.side_window_for(FID).expect("side");
|
||||
core.windows.get_mut(&side).expect("win").view_left += 1;
|
||||
}),
|
||||
("wrap mode", |session| {
|
||||
let mut core = session.state.core.borrow_mut();
|
||||
let side = core.side_window_for(FID).expect("side");
|
||||
let window = core.windows.get_mut(&side).expect("win");
|
||||
window.last_wrap = match window.last_wrap {
|
||||
pmacs::view::WrapMode::Wrap => pmacs::view::WrapMode::Truncate,
|
||||
pmacs::view::WrapMode::Truncate => pmacs::view::WrapMode::Wrap,
|
||||
};
|
||||
}),
|
||||
(
|
||||
"content columns — the gutter is subtracted here",
|
||||
|session| {
|
||||
let mut core = session.state.core.borrow_mut();
|
||||
let side = core.side_window_for(FID).expect("side");
|
||||
core.windows.get_mut(&side).expect("win").last_content_cols += 1;
|
||||
},
|
||||
),
|
||||
("fold PROJECTION POLICY, owned by the view", |session| {
|
||||
let mut core = session.state.core.borrow_mut();
|
||||
let view = core.views.get_mut(&FID).expect("view");
|
||||
view.fold_projection = !view.fold_projection;
|
||||
}),
|
||||
("fold CONTENT, owned by the buffer", |session| {
|
||||
let core = session.state.core.borrow();
|
||||
let side = core.side_window_for(FID).expect("side");
|
||||
let buffer_id = core.windows[&side].buffer_id;
|
||||
let registry = core.registry.clone();
|
||||
let mut reg = registry.borrow_mut();
|
||||
let buffer = reg.get_mut(buffer_id).expect("buffer");
|
||||
let store = core.fold_registry.store_or_attach(buffer);
|
||||
store
|
||||
.lock()
|
||||
.expect("fold store mutex")
|
||||
.insert(pmacs_protocol::ByteRange { start: 0, end: 1 });
|
||||
}),
|
||||
];
|
||||
|
||||
for (name, mutate) in legs {
|
||||
let mut session = Session::new();
|
||||
open_panel(&session, "g2", 4);
|
||||
session.declare(1, 24, 80);
|
||||
let _ = session.present();
|
||||
|
||||
let before = mapping_generation(&mut session).expect("a key");
|
||||
mutate(&session);
|
||||
let after = mapping_generation(&mut session).expect("a key");
|
||||
assert!(
|
||||
after > before,
|
||||
"changing {name} changes which byte a cell means, so the key \
|
||||
must move; it did not ({before} → {after})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// §5b G2 — grid **rows** and **columns** each move the key.
|
||||
///
|
||||
/// **Honest limit, recorded because mutation testing found it:** these
|
||||
/// two legs do NOT discriminate `rows`/`cols` from their product.
|
||||
/// Collapsing the key to `rows * cols` leaves both GREEN, because
|
||||
/// `last_content_cols` co-varies with a column change and the panel's
|
||||
/// row count co-varies with a resize — the key still moves, by another
|
||||
/// input. Only a **transposition** (2×6 → 6×2, identical product) would
|
||||
/// isolate it, and no production path reaches one: rows come from the
|
||||
/// band's height and columns from the frame declaration, and nothing
|
||||
/// swaps them.
|
||||
///
|
||||
/// The key hashes them separately anyway. That is cheap and correct,
|
||||
/// and the alternative — hashing a product because no test can currently
|
||||
/// tell the difference — would be choosing the weaker construction for
|
||||
/// the convenience of the test suite. What these legs *do* pin is that
|
||||
/// each dimension moves the key at all, which is what the rest of the
|
||||
/// slice depends on.
|
||||
#[test]
|
||||
fn g2_grid_rows_and_columns_are_independent_inputs() {
|
||||
// ROWS come from the band's own height, not the frame's total rows —
|
||||
// declaring a shorter frame leaves a 4-row panel a 4-row panel. The
|
||||
// resize path is the one that actually changes them.
|
||||
{
|
||||
let mut session = Session::new();
|
||||
open_panel(&session, "g2rows", 4);
|
||||
session.declare(1, 24, 80);
|
||||
let _ = session.present();
|
||||
|
||||
let before = mapping_generation(&mut session).expect("a key");
|
||||
assert!(
|
||||
session.state.apply_panel_resize_rows(FID, 6),
|
||||
"the resize must be accepted, or this leg proves nothing"
|
||||
);
|
||||
let after = mapping_generation(&mut session).expect("a key");
|
||||
assert!(
|
||||
after > before,
|
||||
"changing grid ROWS alone must move the key — an area product \
|
||||
would miss a transposition"
|
||||
);
|
||||
}
|
||||
|
||||
// COLUMNS come from the declaration.
|
||||
{
|
||||
let mut session = Session::new();
|
||||
open_panel(&session, "g2cols", 4);
|
||||
session.declare(1, 24, 80);
|
||||
let _ = session.present();
|
||||
|
||||
let before = mapping_generation(&mut session).expect("a key");
|
||||
session.declare(2, 24, 40);
|
||||
let after = mapping_generation(&mut session).expect("a key");
|
||||
assert!(
|
||||
after > before,
|
||||
"changing grid COLUMNS alone must move the key"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue