feat(panel): the receiver rows --- dedupe, atomicity, and the TUI control
Closes the rest of this slice's G9-G15 obligations. One production
change, one design correction found by mutation, and the rest witnesses.
G9b is the production change. `panel_motion_is_new` compared the cell
alone, so the FIRST motion after the mapping moved was eaten --- the
pointer has not travelled, but the cell now denotes different text, and
that is exactly the motion the daemon needs to re-anchor the gesture.
Now keyed by `(generation, cell)`. Deliberately read at the motion site
rather than reset from the frame path: resetting on every accepted
repaint would re-arm within one generation and bring pixel-rate traffic
straight back.
G9a, G9c, G10, G10a, G10c are witnesses over behaviour that was already
correct: a generation change ships even when the visible cells are
byte-identical; an identical frame at a higher generation still moves
authority; invalid frames, zero generations and lower generations are
each refused with frame AND generation retained; `Absent` does not erase
the high-water mark.
G15 is the TUI structural control, and its fixture IS the control: the
session has no `SemanticRenderState` at all, so every local click, drag
and wheel effect below is reached without a producer in existence. That
is a stronger claim than asserting a value was not consulted. Both wheel
ticks must land, for the same reason the mapped family needs its
exemption.
The design correction: G10a's first version asserted the zero refusal
with a generation already HELD. Mutating the zero check away left it
green --- zero is also *lower* than the held value, so the nondecreasing
clause refused the frame and the row proved nothing about zero. Zero is
only isolable before any authority exists, which is also the case the
framing names: a sender that never initialised the field. Split into its
own row with that setup, and the row says why.
Mutations, each biting only its named row:
- dedupe compares the cell alone -> G9b
- the frame path re-arms the dedupe on every accepted frame -> G9b
- daemon dedupes across the generation change -> G9a
- return early on frame equality before applying the generation -> G9c
- apply the generation before validating -> G10
- accept generation zero -> G10a (after the split; before it, this
mutation SURVIVED)
- `Absent` erases the high-water mark -> G10c
- a lower generation is accepted -> G10c
- panel input requires a token the TUI cannot have -> G15
Deferred, per SS5b's split table and unchanged here: G11b (exhaustion
cancellation), G12a/G12b (both two-tick wheel EFFECTS), G6c/G7c.
Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; `cargo test --lib` (1959); `cargo test -p pmacs-gpu
--bins` (280); `bottom_panel_stage1_acceptance` (47),
`bottom_panel_stage2b_daemon_acceptance` (39),
`bottom_panel_stage2b_gpu_acceptance` (2); `git diff --check`. Clippy
caught two findings in the new test code after the suites were already
green, which is why it runs as its own step.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
a8a996c446
commit
419b078c0f
|
|
@ -2224,6 +2224,15 @@ struct PanelBand {
|
|||
/// because the first drag after a press must reach the daemon even at the
|
||||
/// cell the press landed on.
|
||||
last_pointer_cell: Option<CellCoord>,
|
||||
/// §5b G9b — the mapping generation `last_pointer_cell` was measured
|
||||
/// under, so the dedupe re-arms across a mapping change.
|
||||
///
|
||||
/// Comparing the cell alone EATS the first motion after the mapping
|
||||
/// moves: the pointer has not travelled, but the cell now denotes
|
||||
/// different text, and that is exactly the motion the daemon needs.
|
||||
/// `None` for a legacy session, where it is constant and the dedupe
|
||||
/// behaves as it always did.
|
||||
last_pointer_generation: Option<u64>,
|
||||
/// Whether the pointer is currently over the divider strip, which
|
||||
/// decides the `RowResize` cursor icon.
|
||||
hover_divider: bool,
|
||||
|
|
@ -7044,6 +7053,7 @@ impl State {
|
|||
self.panel.hover_divider = false;
|
||||
self.panel.pointer_held = false;
|
||||
self.panel.last_pointer_cell = None;
|
||||
self.panel.last_pointer_generation = None;
|
||||
had
|
||||
}
|
||||
// §5b G8b — a mapped session REJECTS the legacy family
|
||||
|
|
@ -7429,10 +7439,18 @@ impl State {
|
|||
/// traffic and every one of those is a daemon-side gesture — the same
|
||||
/// reason the terminal path dedupes.
|
||||
fn panel_motion_is_new(&mut self, coord: CellCoord) -> bool {
|
||||
if self.panel.last_pointer_cell == Some(coord) {
|
||||
// §5b G9b — keyed by generation as well as cell. Deliberately
|
||||
// read here rather than reset from the frame path: resetting on
|
||||
// every accepted repaint would re-arm within one generation and
|
||||
// bring pixel-rate traffic back, and the dedupe would stop
|
||||
// being a dedupe.
|
||||
if self.panel.last_pointer_cell == Some(coord)
|
||||
&& self.panel.last_pointer_generation == self.panel.mapping_generation
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.panel.last_pointer_cell = Some(coord);
|
||||
self.panel.last_pointer_generation = self.panel.mapping_generation;
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -7441,6 +7459,7 @@ impl State {
|
|||
fn set_panel_pointer_held(&mut self, held: bool) {
|
||||
self.panel.pointer_held = held;
|
||||
self.panel.last_pointer_cell = None;
|
||||
self.panel.last_pointer_generation = None;
|
||||
}
|
||||
|
||||
/// Begin a divider drag at surface pixel `y`, if the pointer is on the
|
||||
|
|
@ -20002,6 +20021,327 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// A mapped session with a real installed frame, and the frame.
|
||||
///
|
||||
/// The baseline is installed in the CORRECT family, so every
|
||||
/// assertion below has real authority to preserve rather than a
|
||||
/// `None` that was never set.
|
||||
fn mapped_panel_session(rows: u32, generation: u64) -> Option<(State, PanelFrame)> {
|
||||
let mut state = headless_or_skip(800, 600, "alpha")?;
|
||||
let frame = present_panel(&mut state, rows);
|
||||
state.set_panel_wire(pmacs_protocol::PANEL_MAPPING_MIN_VERSION);
|
||||
let mut mapped = frame;
|
||||
mapped.panel_epoch += 1;
|
||||
assert!(
|
||||
state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: mapped.clone(),
|
||||
mapping_generation: generation,
|
||||
}),
|
||||
"fixture: the mapped baseline must install"
|
||||
);
|
||||
Some((state, mapped))
|
||||
}
|
||||
|
||||
/// §5b G9b — motion dedupe suppresses a repeat WITHIN one generation
|
||||
/// and RE-ARMS across one.
|
||||
///
|
||||
/// The pointer has not moved in either case. What differs is whether
|
||||
/// the cell still denotes the same text: comparing the cell alone
|
||||
/// eats the first motion after the mapping moves, which is precisely
|
||||
/// the motion the daemon needs to re-anchor the gesture.
|
||||
#[test]
|
||||
fn g9b_motion_dedupe_is_keyed_by_generation_not_by_cell_alone() {
|
||||
let Some((mut state, frame)) = mapped_panel_session(4, 5) else {
|
||||
return;
|
||||
};
|
||||
let cell = pmacs_protocol::CellCoord::new(1, 1);
|
||||
|
||||
assert!(
|
||||
state.panel_motion_is_new(cell),
|
||||
"the first motion at a cell is always new"
|
||||
);
|
||||
assert!(
|
||||
!state.panel_motion_is_new(cell),
|
||||
"a repeat within one generation is suppressed — without this \
|
||||
sub-cell motion becomes pixel-rate wire traffic"
|
||||
);
|
||||
|
||||
// A same-generation repaint: a different frame, same key. The
|
||||
// dedupe must NOT re-arm, or the suppression above is defeated
|
||||
// by any style or selection repaint.
|
||||
let mut repaint = frame.clone();
|
||||
repaint.focused = !frame.focused;
|
||||
assert!(
|
||||
state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: repaint,
|
||||
mapping_generation: 5,
|
||||
}),
|
||||
"fixture: the repaint must be accepted, or it proves nothing"
|
||||
);
|
||||
assert!(
|
||||
!state.panel_motion_is_new(cell),
|
||||
"a same-generation repaint does not re-arm the dedupe"
|
||||
);
|
||||
|
||||
// The mapping MOVES. The pointer still has not.
|
||||
let mut rekeyed = frame.clone();
|
||||
rekeyed.focused = frame.focused;
|
||||
assert!(
|
||||
state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: rekeyed,
|
||||
mapping_generation: 6,
|
||||
}),
|
||||
"fixture: the re-keyed frame must be accepted"
|
||||
);
|
||||
assert!(
|
||||
state.panel_motion_is_new(cell),
|
||||
"and across a generation the same cell IS new: it denotes \
|
||||
different text now"
|
||||
);
|
||||
}
|
||||
|
||||
/// §5b G9c — a byte-identical frame at a HIGHER generation still
|
||||
/// updates authority.
|
||||
///
|
||||
/// The visual result is nothing, which is what makes this easy to
|
||||
/// get wrong: returning early on frame equality leaves the frontend
|
||||
/// echoing a generation the daemon has already retired, and every
|
||||
/// gesture it sends is then refused.
|
||||
#[test]
|
||||
fn g9c_an_identical_frame_at_a_higher_generation_still_updates_authority() {
|
||||
let Some((mut state, frame)) = mapped_panel_session(4, 5) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Byte-identical, and the same generation: no work at all.
|
||||
assert!(
|
||||
!state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: frame.clone(),
|
||||
mapping_generation: 5,
|
||||
}),
|
||||
"a true duplicate does nothing"
|
||||
);
|
||||
assert_eq!(state.panel.mapping_generation, Some(5));
|
||||
|
||||
// Byte-identical, HIGHER generation: authority moves.
|
||||
assert!(
|
||||
state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: frame.clone(),
|
||||
mapping_generation: 6,
|
||||
}),
|
||||
"the daemon has re-keyed the mapping under an unchanged \
|
||||
picture — a fold or an edit off-screen does exactly that"
|
||||
);
|
||||
assert_eq!(
|
||||
state.panel.mapping_generation,
|
||||
Some(6),
|
||||
"echoing 5 from here would have every gesture refused"
|
||||
);
|
||||
assert_eq!(
|
||||
state.panel.frame.as_ref(),
|
||||
Some(&frame),
|
||||
"and the frame is unchanged, because it was identical"
|
||||
);
|
||||
}
|
||||
|
||||
/// §5b G10 — a structurally invalid mapped frame is refused
|
||||
/// ATOMICALLY: previous frame and previous generation both retained.
|
||||
///
|
||||
/// Updating one without the other leaves a valid generation naming a
|
||||
/// frame that was never shown, which is the same stale-mapping hole
|
||||
/// from the receiving side.
|
||||
#[test]
|
||||
fn g10_an_invalid_mapped_frame_is_refused_atomically() {
|
||||
let Some((mut state, frame)) = mapped_panel_session(4, 5) else {
|
||||
return;
|
||||
};
|
||||
state.set_panel_pointer_held(true);
|
||||
state.panel.last_pointer_cell = Some(pmacs_protocol::CellCoord::new(1, 1));
|
||||
|
||||
// G10 — structurally invalid: the cell count contradicts `size`.
|
||||
let mut invalid = frame.clone();
|
||||
invalid.cells.pop();
|
||||
assert!(
|
||||
invalid.validate().is_err(),
|
||||
"fixture: the frame must actually be invalid"
|
||||
);
|
||||
assert!(
|
||||
!state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: invalid,
|
||||
mapping_generation: 9,
|
||||
})
|
||||
);
|
||||
assert_eq!(state.panel.frame.as_ref(), Some(&frame), "frame retained");
|
||||
assert_eq!(
|
||||
state.panel.mapping_generation,
|
||||
Some(5),
|
||||
"and its generation with it — a valid 9 here would name a \
|
||||
frame this frontend never received"
|
||||
);
|
||||
|
||||
// G10a — structurally VALID, generation zero. Zero is what a
|
||||
// sender that never initialised the field produces; accepting it
|
||||
// once disables the check for the session.
|
||||
let mut zeroed = frame.clone();
|
||||
zeroed.panel_epoch += 7;
|
||||
assert!(
|
||||
zeroed.validate().is_ok(),
|
||||
"fixture: this one is structurally fine, so the refusal is \
|
||||
about the zero"
|
||||
);
|
||||
assert!(
|
||||
!state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: zeroed,
|
||||
mapping_generation: 0,
|
||||
})
|
||||
);
|
||||
assert_eq!(state.panel.frame.as_ref(), Some(&frame));
|
||||
assert_eq!(state.panel.mapping_generation, Some(5));
|
||||
assert!(state.panel.pointer_held, "a refusal ends no live gesture");
|
||||
assert_eq!(
|
||||
state.panel.last_pointer_cell,
|
||||
Some(pmacs_protocol::CellCoord::new(1, 1)),
|
||||
"nor discards its dedupe baseline"
|
||||
);
|
||||
}
|
||||
|
||||
/// §5b G10a — a ZERO generation is refused when NO authority is
|
||||
/// held yet.
|
||||
///
|
||||
/// This is the only setup that isolates the zero check. With a
|
||||
/// generation already held, zero is also *lower* than it, so the
|
||||
/// nondecreasing clause refuses the frame and a row built that way
|
||||
/// stays green with the zero check deleted — measured, not assumed.
|
||||
///
|
||||
/// The case is the one the framing names: a sender that never
|
||||
/// initialised the field. Accepting it once records `Some(0)`, and
|
||||
/// from there no later frame is ever "lower", so the check is
|
||||
/// disabled for the whole session.
|
||||
#[test]
|
||||
fn g10a_a_zero_generation_is_refused_before_any_authority_is_held() {
|
||||
let Some(mut state) = headless_or_skip(800, 600, "alpha") else {
|
||||
return;
|
||||
};
|
||||
// A legacy frame first, so there is a real retained band to
|
||||
// preserve, then the family switch. `mapping_generation` is
|
||||
// genuinely `None` here — that is the state under test, not an
|
||||
// accident of the fixture.
|
||||
let baseline = present_panel(&mut state, 4);
|
||||
state.set_panel_wire(pmacs_protocol::PANEL_MAPPING_MIN_VERSION);
|
||||
assert_eq!(
|
||||
state.panel.mapping_generation, None,
|
||||
"fixture: no authority held yet"
|
||||
);
|
||||
|
||||
let mut zeroed = baseline.clone();
|
||||
zeroed.panel_epoch += 1;
|
||||
assert!(
|
||||
zeroed.validate().is_ok(),
|
||||
"fixture: structurally fine, so the refusal is about the zero"
|
||||
);
|
||||
assert!(
|
||||
!state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: zeroed,
|
||||
mapping_generation: 0,
|
||||
}),
|
||||
"zero is the wire's uninitialised value and is never authority"
|
||||
);
|
||||
assert_eq!(
|
||||
state.panel.mapping_generation, None,
|
||||
"and nothing is recorded — `Some(0)` would make every later \
|
||||
frame non-lower and disable the check for the session"
|
||||
);
|
||||
assert_eq!(
|
||||
state.panel.frame.as_ref().map(|f| f.panel_epoch),
|
||||
Some(baseline.panel_epoch),
|
||||
"the retained band survives the refusal"
|
||||
);
|
||||
|
||||
// POSITIVE CONTROL — the same frame with a real generation
|
||||
// installs, so the refusal is about the zero and not about the
|
||||
// family switch.
|
||||
let mut real = baseline.clone();
|
||||
real.panel_epoch += 2;
|
||||
assert!(state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: real,
|
||||
mapping_generation: 1,
|
||||
}));
|
||||
assert_eq!(state.panel.mapping_generation, Some(1));
|
||||
}
|
||||
|
||||
/// §5b G10c — the generation is NONDECREASING, and `Absent` does not
|
||||
/// erase the high-water mark.
|
||||
///
|
||||
/// Values may SKIP: the daemon advances the key on mapping changes,
|
||||
/// not on frames, so a frontend that misses two repaints legitimately
|
||||
/// jumps. What may never happen is going backward — a delayed frame
|
||||
/// must not roll this frontend's authority back to a mapping that is
|
||||
/// gone.
|
||||
#[test]
|
||||
fn g10c_generation_is_nondecreasing_and_survives_absent() {
|
||||
let Some((mut state, frame)) = mapped_panel_session(4, 5) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// A SKIP forward is fine.
|
||||
let mut skipped = frame.clone();
|
||||
skipped.panel_epoch += 1;
|
||||
assert!(state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: skipped.clone(),
|
||||
mapping_generation: 9,
|
||||
}));
|
||||
assert_eq!(state.panel.mapping_generation, Some(9));
|
||||
|
||||
// A LOWER one is refused, atomically.
|
||||
let mut delayed = frame.clone();
|
||||
delayed.panel_epoch += 2;
|
||||
assert!(
|
||||
!state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: delayed.clone(),
|
||||
mapping_generation: 8,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
state.panel.frame.as_ref().map(|f| f.panel_epoch),
|
||||
Some(skipped.panel_epoch),
|
||||
"the retained frame survives"
|
||||
);
|
||||
assert_eq!(state.panel.mapping_generation, Some(9));
|
||||
|
||||
// An EQUAL generation with a different frame is accepted: a
|
||||
// style or selection repaint does not move the mapping, and
|
||||
// refusing it would freeze the band.
|
||||
let mut repaint = skipped.clone();
|
||||
repaint.focused = !skipped.focused;
|
||||
assert!(
|
||||
state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: repaint,
|
||||
mapping_generation: 9,
|
||||
}),
|
||||
"an equal-generation repaint is valid"
|
||||
);
|
||||
assert_eq!(state.panel.mapping_generation, Some(9));
|
||||
|
||||
// `Absent` hides the band but must NOT erase the high-water
|
||||
// mark: a frame delayed across the hide would otherwise come
|
||||
// back believed.
|
||||
assert!(state.apply_panel_payload(PanelFramePayload::Absent));
|
||||
assert!(state.panel.presented().is_none(), "the band is down");
|
||||
assert_eq!(
|
||||
state.panel.mapping_generation,
|
||||
Some(9),
|
||||
"and the high-water mark stands"
|
||||
);
|
||||
assert!(
|
||||
!state.apply_panel_payload(PanelFramePayload::PresentMapped {
|
||||
frame: delayed,
|
||||
mapping_generation: 8,
|
||||
}),
|
||||
"so the delayed frame is still refused after the hide"
|
||||
);
|
||||
assert!(state.panel.presented().is_none(), "and stays down");
|
||||
}
|
||||
|
||||
/// §5b G8b — a MAPPED frontend refuses the legacy family, and the
|
||||
/// refusal is ATOMIC.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1442,6 +1442,223 @@ fn g1_a_foreign_edit_moves_the_mapping_key_before_anything_renders() {
|
|||
);
|
||||
}
|
||||
|
||||
/// §5b G15 — **TUI structural control**: the local panel click, drag
|
||||
/// and wheel paths keep their effects with NO mapping generation
|
||||
/// anywhere.
|
||||
///
|
||||
/// The TUI hit-tests the daemon's own state directly. It receives no
|
||||
/// `PanelFrame`, so it has no generation to echo and there is nothing
|
||||
/// for a freshness check to compare. If the check ever migrates from
|
||||
/// the authenticated semantic boundary into shared replay, local input
|
||||
/// stops working entirely — and it would stop silently, because
|
||||
/// refusing a gesture looks exactly like a gesture that did nothing.
|
||||
///
|
||||
/// The control is the FIXTURE: this session has no
|
||||
/// `SemanticRenderState` at all. Every effect below is therefore
|
||||
/// reached without a producer in existence, which is the strongest form
|
||||
/// of "no generation was consulted" — not an assertion about a value,
|
||||
/// but the absence of anything that could hold one.
|
||||
#[test]
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
reason = "one control over three input kinds: splitting it would \
|
||||
give each leg its own fixture, and the shared fixture — a \
|
||||
session with no SemanticRenderState at all — is the control"
|
||||
)]
|
||||
fn g15_local_tui_panel_input_keeps_its_effects_with_no_generation() {
|
||||
use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind as TuiMouseKind};
|
||||
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, "pmacs.lsp.config = {}");
|
||||
state.sync_frame_geometry(FrontendId::LOCAL, CellSize::new(ROWS, COLS));
|
||||
exec(
|
||||
&state,
|
||||
"PANEL_BUF = pmacs.buffer.create(\"*g15*\")
|
||||
pmacs.buffer.set_generated_contents(PANEL_BUF, \
|
||||
\"alpha alpha\\nbravo bravo\\ncharlie\\ndelta\\necho\\nfox\\ngolf\\nhotel\\n\")
|
||||
pmacs.window.display(PANEL_BUF, { side = \"bottom\", height = 4 })",
|
||||
);
|
||||
let mut state = state;
|
||||
let panel = state
|
||||
.core
|
||||
.borrow()
|
||||
.side_window_for(FrontendId::LOCAL)
|
||||
.expect("a panel");
|
||||
let document = state
|
||||
.core
|
||||
.borrow()
|
||||
.non_side_target(FrontendId::LOCAL)
|
||||
.expect("a document");
|
||||
|
||||
// Paint one frame first, exactly as the TUI does before a user can
|
||||
// click anything. The window's text view is built from the buffer at
|
||||
// paint time, so input dispatched against an unpainted window maps
|
||||
// every row to offset zero and the row proves nothing.
|
||||
{
|
||||
let mut cells = vec![pmacs::cell::Cell::default(); (ROWS * COLS) as usize];
|
||||
let mut grid = pmacs::cell::CellGrid {
|
||||
cells: &mut cells,
|
||||
stride: COLS,
|
||||
size: CellSize::new(ROWS, COLS),
|
||||
};
|
||||
pmacs::editor::paint_frame(
|
||||
&state,
|
||||
FrontendId::LOCAL,
|
||||
&HashMap::new(),
|
||||
&mut grid,
|
||||
CellSize::new(ROWS, COLS),
|
||||
);
|
||||
}
|
||||
|
||||
// Where the panel actually is, from the same layout the TUI paints.
|
||||
let panel_rect = {
|
||||
let core = state.core.borrow();
|
||||
let view = core.views.get(&FrontendId::LOCAL).expect("LOCAL view");
|
||||
let area = pmacs::window::Rect::new(0, 0, ROWS - 1, COLS);
|
||||
let fixed = core.panel_fixed_rows(FrontendId::LOCAL, area.size.rows);
|
||||
view.layout.compute(area, &fixed)[&panel]
|
||||
};
|
||||
let row = u16::try_from(panel_rect.origin.row + 1).expect("row fits");
|
||||
// Past the gutter, and inside a line that is long enough for the
|
||||
// drag below to stay within it: clamping at the line end would make
|
||||
// press and drag land on the same byte and prove nothing.
|
||||
let gutter = state.core.borrow().windows[&panel].gutter_width();
|
||||
let col = u16::try_from(panel_rect.origin.col + gutter + 1).expect("col fits");
|
||||
let click = |kind| MouseEvent {
|
||||
kind,
|
||||
column: col,
|
||||
row,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
};
|
||||
|
||||
// Precondition, scoped to THIS view: the panel is not already
|
||||
// focused, or every assertion below would pass without input.
|
||||
assert_eq!(
|
||||
state.core.borrow().views[&FrontendId::LOCAL].active,
|
||||
document,
|
||||
"precondition: the document is focused"
|
||||
);
|
||||
|
||||
// CLICK — focuses the panel and positions the cursor.
|
||||
state.dispatch_mouse(
|
||||
FrontendId::LOCAL,
|
||||
click(TuiMouseKind::Down(MouseButton::Left)),
|
||||
CellSize::new(ROWS, COLS),
|
||||
);
|
||||
assert_eq!(
|
||||
state.core.borrow().views[&FrontendId::LOCAL].active,
|
||||
panel,
|
||||
"a local press focuses the panel — no token, no refusal"
|
||||
);
|
||||
let pressed_cursor = state.core.borrow().windows[&panel].cursor;
|
||||
assert_ne!(
|
||||
pressed_cursor, 0,
|
||||
"the press positioned the cursor inside the text, not at the \
|
||||
buffer start — a row that pressed into an unpainted window \
|
||||
would read zero here and never notice"
|
||||
);
|
||||
|
||||
// DRAG — extends a selection inside the panel.
|
||||
state.dispatch_mouse(
|
||||
FrontendId::LOCAL,
|
||||
MouseEvent {
|
||||
kind: TuiMouseKind::Drag(MouseButton::Left),
|
||||
column: col + 7,
|
||||
row,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
},
|
||||
CellSize::new(ROWS, COLS),
|
||||
);
|
||||
assert!(
|
||||
state.core.borrow().windows[&panel].selection.is_some(),
|
||||
"a local drag selects inside the panel"
|
||||
);
|
||||
assert_ne!(
|
||||
state.core.borrow().windows[&panel].cursor,
|
||||
pressed_cursor,
|
||||
"and the drag moved the cursor, so the selection is a real range"
|
||||
);
|
||||
|
||||
state.dispatch_mouse(
|
||||
FrontendId::LOCAL,
|
||||
MouseEvent {
|
||||
kind: TuiMouseKind::Up(MouseButton::Left),
|
||||
column: col + 7,
|
||||
row,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
},
|
||||
CellSize::new(ROWS, COLS),
|
||||
);
|
||||
|
||||
// WHEEL — two ticks, both of which must land. The mapped family's
|
||||
// wheel exemption exists so this stays true over the wire; here
|
||||
// there is no wire, and it must be true for the same reason.
|
||||
let before = state.core.borrow().windows[&panel].view_top;
|
||||
for _ in 0..2 {
|
||||
state.dispatch_mouse(
|
||||
FrontendId::LOCAL,
|
||||
click(TuiMouseKind::ScrollDown),
|
||||
CellSize::new(ROWS, COLS),
|
||||
);
|
||||
}
|
||||
assert_ne!(
|
||||
state.core.borrow().windows[&panel].view_top,
|
||||
before,
|
||||
"a local wheel scrolls the panel"
|
||||
);
|
||||
}
|
||||
|
||||
/// §5b G9a — a generation change is EMITTED even when the picture is
|
||||
/// byte-identical.
|
||||
///
|
||||
/// The panel shows four rows; an edit further down the buffer moves the
|
||||
/// key without changing a single visible cell. Suppressing that frame
|
||||
/// because the cells match would leave the frontend echoing a
|
||||
/// generation the daemon has already retired, and every gesture it then
|
||||
/// sends is refused — the panel goes quietly dead while looking
|
||||
/// perfectly correct.
|
||||
#[test]
|
||||
fn g9a_a_generation_change_ships_even_when_the_cells_are_identical() {
|
||||
let mut session = Session::new();
|
||||
open_panel(&session, "g9a", 4);
|
||||
session.declare(1, 24, 80);
|
||||
|
||||
// Enough lines that the visible four cannot see the edit below.
|
||||
let mut lines = String::new();
|
||||
for line in 0..20 {
|
||||
use std::fmt::Write as _;
|
||||
writeln!(lines, "line {line}").expect("writing to a String never fails");
|
||||
}
|
||||
foreign_edit(&session, &lines);
|
||||
let baseline = session.present();
|
||||
let before = mapping_generation(&mut session).expect("a live key");
|
||||
|
||||
// Change line 15 only. Rows 0..4 are untouched.
|
||||
let edited = lines.replace("line 15\n", "LINE 15 CHANGED\n");
|
||||
assert_ne!(edited, lines, "fixture: the edit must actually apply");
|
||||
foreign_edit(&session, &edited);
|
||||
|
||||
let payload = session
|
||||
.frame()
|
||||
.expect("a generation change is not silence: the frame must ship");
|
||||
let (frame, shipped) = match payload {
|
||||
PanelFramePayload::PresentMapped {
|
||||
frame,
|
||||
mapping_generation,
|
||||
} => (frame, mapping_generation),
|
||||
other => panic!("a v25 producer ships the mapped family, got {other:?}"),
|
||||
};
|
||||
assert_eq!(
|
||||
frame.cells, baseline.cells,
|
||||
"fixture: the visible cells really are identical — if they \
|
||||
differ, this row is testing an ordinary repaint instead"
|
||||
);
|
||||
assert!(
|
||||
shipped > before,
|
||||
"and the key moved with the edit below the fold"
|
||||
);
|
||||
}
|
||||
|
||||
/// §5b G3 — the **stable** inputs, one row each.
|
||||
///
|
||||
/// Every entry here is something that repaints a panel without changing
|
||||
|
|
|
|||
Loading…
Reference in New Issue