feat(panel): G5a's cancellation trigger, and the latch it needs
G5a is the one G5 row this slice owns: the mapping key advancing must
raise cancellation at the advance, not reactively when a later event is
refused. Reactive cancellation loses a race --- if the successor frame
reaches the frontend before the physical `Up`, the producer clears its
latch and the cancelling event never arrives.
Cancelling requires something to cancel, so the accepted-gesture latch
lands with it: `AcceptedPanelGesture` (button, coord, buffer, and
whether the press reached a child) in a per-frontend slot on
`SemanticRenderState`, armed from the daemon's accepted inbound arms
after every gate has passed.
Writing that code decides three things the framing's deferred rows later
assert about, so they are pinned here under SUBSTRATE names rather than
under G5c/G5d/G5g/G5p. Those IDs stay on `panel-pointer-replay` per
SS5b's split table --- each asserts something about a synthetic release
or a real drag continuation that does not exist on this base, and
claiming an ID in two branches is the merge hazard `active-work.md`
already records surviving a clean merge once.
Two consequences are recorded rather than fixed:
- Cancellations are COUNTED, not queued. The record queue is what
replay drains to deliver each release; landing it here would grow
one entry per cancelled drag with nothing ever draining it. A
saturating count is bounded and still separates a consume from a
cancellation.
- The other G5b transitions --- panel epoch, buffer replacement,
same-size geometry, detach --- leave the latch armed on this base.
Each strands a live gesture whose release can never be accepted.
That is inert while nothing consumes the latch, and becomes a defect
exactly when replay supplies effects, in the branch that owns the
row. `Absent` is wired anyway, because `publish_absent_panel`
clears input authority two lines later; leaving it out would be an
inconsistency inside one function rather than a clean deferral.
Five mutations, each biting only its named row:
- drop the advance trigger (reactive cancellation) -> G5a alone
- arm on every accepted pointer event -> the arming substrate alone
- an ordinary `Up` no longer consumes -> the arming substrate alone
- a consume counts as a cancellation -> the arming substrate alone
- one global latch via a shared slot -> the ownership substrate alone
Also repairs a fourth rustdoc split on this branch. Inserting
`AcceptedPanelGesture` at what read as a blank gap adopted
`SemanticRenderState`'s doc comment AND its
`#[allow(clippy::struct_excessive_bools)]`, silently un-suppressing a
lint on the struct that needed it. Same mechanism all four times; the
ledger now records the check as "look UP from the insertion point".
Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; `cargo test --lib` (1956 passed); the two
`bottom_panel_stage2b_*` acceptance suites (39 passed); `git diff
--check`. The full eleven-stage `--protocol` gate is reserved for slice
completion per the standing procedure.
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
a242038e8a
commit
8c5ca10698
|
|
@ -281,8 +281,13 @@ from #171 and #215.
|
||||||
**`githubsucks/panel-mapping-generation` is the authoritative tip.**
|
**`githubsucks/panel-mapping-generation` is the authoritative tip.**
|
||||||
Recover with `git fetch githubsucks && git checkout
|
Recover with `git fetch githubsucks && git checkout
|
||||||
panel-mapping-generation`.
|
panel-mapping-generation`.
|
||||||
- **No PR yet. Checkpoint: framing revision 16 (§5b) APPROVED
|
- **No PR yet. Checkpoint: `3c06176`** — framing revision 16 (§5b)
|
||||||
2026-08-15; NO IMPLEMENTATION.**
|
APPROVED 2026-08-15 at `7e85a6f`, then eleven implementation commits:
|
||||||
|
wire shapes and pins (G0), the authoritative key with its terminal
|
||||||
|
half (G1–G4), outbound family selection, inbound family gating, the
|
||||||
|
GPU's negotiated family enum, and the nine family-gate rows
|
||||||
|
(G6a–b, G7a–b, G8). **Full `--protocol` gate green at `3c06176`.**
|
||||||
|
In progress since, uncommitted: G5's accepted-gesture latch.
|
||||||
- **PROTOCOL-BEARING — v25, and it runs alone.**
|
- **PROTOCOL-BEARING — v25, and it runs alone.**
|
||||||
`ADVERTISED_PROTOCOL_VERSION` stays pinned at **20**.
|
`ADVERTISED_PROTOCOL_VERSION` stays pinned at **20**.
|
||||||
- **Why it exists.** A `PanelPointer` names a cell and nothing on the
|
- **Why it exists.** A `PanelPointer` names a cell and nothing on the
|
||||||
|
|
@ -384,6 +389,22 @@ from #171 and #215.
|
||||||
invalidations and every document, terminal, complete producer-latch
|
invalidations and every document, terminal, complete producer-latch
|
||||||
and click-chain effect through real call sites. No dead
|
and click-chain effect through real call sites. No dead
|
||||||
classification helper is added on a base without replay.
|
classification helper is added on a base without replay.
|
||||||
|
- **G5's latch lands here under SUBSTRATE names, not the deferred
|
||||||
|
IDs.** G5a needs something to cancel, so the per-frontend
|
||||||
|
`AcceptedPanelGesture` latch and its arming from the accepted inbound
|
||||||
|
arms are this slice's. Writing that code decides which events arm,
|
||||||
|
that an ordinary `Up` consumes without counting as a cancellation,
|
||||||
|
and that the latch is per frontend — pinned as `g5_substrate_*`
|
||||||
|
rather than as G5c/G5d/G5g/G5p, which stay on the replay lane per
|
||||||
|
§5b's split table. **Claiming an ID in two branches is the merge
|
||||||
|
hazard**, and this file already records duplicate ids surviving a
|
||||||
|
clean merge once. Cancellations are **counted, not queued**: the
|
||||||
|
record queue is what replay drains, and landing it here would grow
|
||||||
|
one entry per cancelled drag with no reader. The other G5b
|
||||||
|
transitions (panel epoch, buffer replacement, same-size geometry,
|
||||||
|
detach) deliberately leave the latch armed on this base — inert
|
||||||
|
while nothing consumes it, and a defect only once replay supplies
|
||||||
|
effects, in the branch that owns the row.
|
||||||
- **Routing and effects are separate controls.** This slice can prove
|
- **Routing and effects are separate controls.** This slice can prove
|
||||||
v24/v25 events reach the existing focus path, but selection,
|
v24/v25 events reach the existing focus path, but selection,
|
||||||
terminal reporting, stable-generation drag continuation, two-tick
|
terminal reporting, stable-generation drag continuation, two-tick
|
||||||
|
|
@ -450,6 +471,15 @@ from #171 and #215.
|
||||||
three of them lost to these two signatures. U9's synthetic-load
|
three of them lost to these two signatures. U9's synthetic-load
|
||||||
control remains unrun and is the cheapest thing that would either
|
control remains unrun and is the cheapest thing that would either
|
||||||
implicate load or clear it.
|
implicate load or clear it.
|
||||||
|
- **Rustdoc split, FOUR occurrences on this branch** (`screen_size`,
|
||||||
|
`peer_may_send_panel_events`, `send_panel_pointer`, and
|
||||||
|
`SemanticRenderState`). Always the same mechanism: inserting an item
|
||||||
|
at what reads as a blank gap when the lines directly above are the
|
||||||
|
NEXT item's doc comment, which the insertion then adopts. The fourth
|
||||||
|
also stole an `#[allow(clippy::struct_excessive_bools)]`, silently
|
||||||
|
un-suppressing a lint on the struct that needed it. Three were caught
|
||||||
|
by the user in review, one by a `missing_docs` warning. **The check
|
||||||
|
is to look UP from the insertion point before writing, not down.**
|
||||||
- **Gates:** the four `bottom_panel_*` suites, the GUI 1a wire suite,
|
- **Gates:** the four `bottom_panel_*` suites, the GUI 1a wire suite,
|
||||||
`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`, and **`--protocol`**.
|
`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`, and **`--protocol`**.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2460,6 +2460,43 @@ lane must run the whole G5 matrix through real document and terminal
|
||||||
call sites. This is the same producer-emits/receiver-discards blind
|
call sites. This is the same producer-emits/receiver-discards blind
|
||||||
spot that forced the replay prerequisite in the first place.
|
spot that forced the replay prerequisite in the first place.
|
||||||
|
|
||||||
|
#### The latch substrate that lands here, and what it does not claim
|
||||||
|
|
||||||
|
G5a needs something to cancel, so the accepted-gesture latch itself —
|
||||||
|
`AcceptedPanelGesture` and the per-frontend slot on
|
||||||
|
`SemanticRenderState` — lands on this slice, armed from the daemon's
|
||||||
|
accepted inbound arms. Writing that code decides three things the
|
||||||
|
deferred rows later assert about, so those decisions are pinned here
|
||||||
|
under **substrate** names (`g5_substrate_*`) rather than under G5c,
|
||||||
|
G5d, G5g or G5p:
|
||||||
|
|
||||||
|
- which events arm and which consume (`update_accepted_gesture`);
|
||||||
|
- that an ordinary `Up` consumes **without** counting as a
|
||||||
|
cancellation;
|
||||||
|
- that the latch is per frontend, so one frontend's loss cannot empty
|
||||||
|
another's.
|
||||||
|
|
||||||
|
The framing rows keep their IDs on `panel-pointer-replay`, because each
|
||||||
|
of them asserts something about a synthetic release or a real drag
|
||||||
|
continuation that does not exist on this base. Two IDs claimed in two
|
||||||
|
branches is the merge hazard; substrate names avoid it.
|
||||||
|
|
||||||
|
Two consequences are recorded rather than fixed:
|
||||||
|
|
||||||
|
- **Cancellations are counted, not queued.** A queue of records is what
|
||||||
|
replay drains to deliver each release; landing it here would grow one
|
||||||
|
entry per cancelled drag with nothing ever draining it. A saturating
|
||||||
|
count is bounded and still separates a consume from a cancellation.
|
||||||
|
- **The other G5b transitions leave the latch armed on this base.** A
|
||||||
|
panel-epoch change, a buffer replacement, a same-size geometry change
|
||||||
|
and a detach all strand a live gesture — its release can never be
|
||||||
|
accepted. That is inert here, because nothing consumes the latch, and
|
||||||
|
becomes a defect exactly when replay gives it effects, in the branch
|
||||||
|
that owns the row. `Absent` is wired anyway, because
|
||||||
|
`publish_absent_panel` already clears input authority two lines
|
||||||
|
later and leaving that one out would be an inconsistency inside a
|
||||||
|
single function rather than a clean deferral.
|
||||||
|
|
||||||
### Coherence impact (`COHERENCE.md` §20)
|
### Coherence impact (`COHERENCE.md` §20)
|
||||||
|
|
||||||
- **Journey steps touched: 5** (document-panel editing and selection)
|
- **Journey steps touched: 5** (document-panel editing and selection)
|
||||||
|
|
|
||||||
283
src/daemon.rs
283
src/daemon.rs
|
|
@ -1029,6 +1029,50 @@ fn panel_event_epochs_are_current(
|
||||||
/// Consulted BEFORE the payload is trusted, before any generation is
|
/// Consulted BEFORE the payload is trusted, before any generation is
|
||||||
/// validated and before any mutation: the family decides which variant
|
/// validated and before any mutation: the family decides which variant
|
||||||
/// is even admissible, so it cannot depend on the variant's contents.
|
/// is even admissible, so it cannot depend on the variant's contents.
|
||||||
|
/// §5b G5c/G5d/G5g — update the accepted-gesture latch for one
|
||||||
|
/// ACCEPTED panel gesture.
|
||||||
|
///
|
||||||
|
/// Called only after every gate has passed, so "accepted" means exactly
|
||||||
|
/// that. Three rules, and each closes its own hole:
|
||||||
|
///
|
||||||
|
/// * only a left `Down` ARMS — a right press opens a menu and ends
|
||||||
|
/// there, and `Move`, wheel and the other buttons begin nothing, so
|
||||||
|
/// arming on them would manufacture a delayed release at the next
|
||||||
|
/// authority loss (G5g);
|
||||||
|
/// * a left `Up` CONSUMES, or a later invalidation finds a gesture it
|
||||||
|
/// believes live and duplicates its release (G5c);
|
||||||
|
/// * an `Up` with no armed gesture is INERT — it terminates nothing,
|
||||||
|
/// because nothing began (G5d).
|
||||||
|
fn update_accepted_gesture(
|
||||||
|
semantic_states: &mut HashMap<FrontendId, crate::semantic_render::SemanticRenderState>,
|
||||||
|
source: FrontendId,
|
||||||
|
kind: pmacs_protocol::MouseKind,
|
||||||
|
coord: pmacs_protocol::CellCoord,
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
reached_child: bool,
|
||||||
|
) {
|
||||||
|
use pmacs_protocol::{MouseButton, MouseKind};
|
||||||
|
let Some(state) = semantic_states.get_mut(&source) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match kind {
|
||||||
|
MouseKind::Down(MouseButton::Left) => {
|
||||||
|
state.arm_accepted_gesture(crate::semantic_render::AcceptedPanelGesture {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
coord,
|
||||||
|
buffer_id,
|
||||||
|
reached_child,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
MouseKind::Up(MouseButton::Left) => {
|
||||||
|
// Inert when nothing was armed: `take` on `None` is the
|
||||||
|
// whole of G5d.
|
||||||
|
let _ = state.consume_accepted_gesture();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn peer_uses_mapped_panel_family(session_registry: &SessionRegistry, source: FrontendId) -> bool {
|
fn peer_uses_mapped_panel_family(session_registry: &SessionRegistry, source: FrontendId) -> bool {
|
||||||
session_registry
|
session_registry
|
||||||
.session_state(source)
|
.session_state(source)
|
||||||
|
|
@ -2507,6 +2551,17 @@ fn handle_dispatcher_event(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
editor.dispatch_semantic_panel_pointer(source, buffer_id, coord, kind);
|
editor.dispatch_semantic_panel_pointer(source, buffer_id, coord, kind);
|
||||||
|
update_accepted_gesture(
|
||||||
|
semantic_states,
|
||||||
|
source,
|
||||||
|
kind,
|
||||||
|
coord,
|
||||||
|
buffer_id,
|
||||||
|
// Whether the press reached a child is
|
||||||
|
// replay's to know; until replay exists no
|
||||||
|
// press does.
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FrontendEvent::PanelPointerMapped {
|
FrontendEvent::PanelPointerMapped {
|
||||||
|
|
@ -2551,6 +2606,14 @@ fn handle_dispatcher_event(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
editor.dispatch_semantic_panel_pointer(source, buffer_id, coord, kind);
|
editor.dispatch_semantic_panel_pointer(source, buffer_id, coord, kind);
|
||||||
|
update_accepted_gesture(
|
||||||
|
semantic_states,
|
||||||
|
source,
|
||||||
|
kind,
|
||||||
|
coord,
|
||||||
|
buffer_id,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FrontendEvent::Pointer {
|
FrontendEvent::Pointer {
|
||||||
|
|
@ -6670,6 +6733,226 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// §5b G5a — the key advancing RAISES cancellation, without waiting
|
||||||
|
/// for another pointer event.
|
||||||
|
///
|
||||||
|
/// The daemon-side half. The EFFECTS — clearing an empty selection,
|
||||||
|
/// clearing the click chain, delivering the child's release — are
|
||||||
|
/// owed by `panel-pointer-replay`, which is the only branch where
|
||||||
|
/// replay exists; this pins the trigger and the record it produces.
|
||||||
|
#[test]
|
||||||
|
fn g5a_a_mapping_change_cancels_a_live_gesture_with_no_further_event() {
|
||||||
|
let fid = FrontendId(770);
|
||||||
|
let (mut editor, mut semantic_states, mut render, _document, panel, epochs) =
|
||||||
|
panel_session_at(PROTOCOL_VERSION, fid);
|
||||||
|
let buffer_id = editor.core.borrow().windows[&panel].buffer_id;
|
||||||
|
let generation = stamped_generation(&semantic_states, fid);
|
||||||
|
|
||||||
|
dispatch_panel_event(
|
||||||
|
&mut editor,
|
||||||
|
fid,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
&mut semantic_states,
|
||||||
|
&mut render,
|
||||||
|
mapped_pointer(fid, epochs, buffer_id, generation),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
semantic_states[&fid].has_accepted_gesture(),
|
||||||
|
"fixture: an accepted left press arms the latch"
|
||||||
|
);
|
||||||
|
|
||||||
|
// A FOREIGN edit — nothing this frontend did, and no further
|
||||||
|
// pointer event. The key moves on the next read.
|
||||||
|
{
|
||||||
|
let core = editor.core.borrow();
|
||||||
|
let registry = core.registry.clone();
|
||||||
|
let mut reg = registry.borrow_mut();
|
||||||
|
reg.get_mut(buffer_id)
|
||||||
|
.expect("panel buffer")
|
||||||
|
.set_generated_contents(b"moved\n")
|
||||||
|
.expect("a plain content change");
|
||||||
|
}
|
||||||
|
let snapshot = editor.panel_mapping_snapshot(fid);
|
||||||
|
let state = semantic_states.get_mut(&fid).expect("projection");
|
||||||
|
let advanced = state.panel_mapping_generation(snapshot);
|
||||||
|
assert_ne!(advanced, Some(generation), "the key must have moved");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!state.has_accepted_gesture(),
|
||||||
|
"the advance itself ends the gesture — waiting for another \
|
||||||
|
event loses the race where the successor frame lands first"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.panel_gesture_cancellations(),
|
||||||
|
1,
|
||||||
|
"and it records exactly one cancellation for replay to \
|
||||||
|
terminate — an ordinary consume would leave this at zero"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// §5b — SUBSTRATE for the framing's G5c/G5d/G5g: the latch's
|
||||||
|
/// arming rules, as this branch's inbound arms decide them.
|
||||||
|
///
|
||||||
|
/// Deliberately not named for those rows. G5c, G5d and G5g are
|
||||||
|
/// `panel-pointer-replay`'s per §5b's split table, and each asserts
|
||||||
|
/// something about a synthetic release that does not exist on this
|
||||||
|
/// branch. What is provable here is narrower and still worth
|
||||||
|
/// pinning, because `update_accepted_gesture` decides it: which
|
||||||
|
/// events arm, which consume, and that a consume is not a
|
||||||
|
/// cancellation.
|
||||||
|
#[test]
|
||||||
|
fn g5_substrate_only_an_accepted_left_press_arms_and_a_release_consumes() {
|
||||||
|
let fid = FrontendId(771);
|
||||||
|
let (mut editor, mut semantic_states, mut render, _document, panel, epochs) =
|
||||||
|
panel_session_at(PROTOCOL_VERSION, fid);
|
||||||
|
let buffer_id = editor.core.borrow().windows[&panel].buffer_id;
|
||||||
|
let generation = stamped_generation(&semantic_states, fid);
|
||||||
|
let mut send = |kind, semantic_states: &mut HashMap<_, _>, editor: &mut _| {
|
||||||
|
let mut event = mapped_pointer(fid, epochs, buffer_id, generation);
|
||||||
|
if let FrontendEvent::PanelPointerMapped { kind: k, .. } = &mut event {
|
||||||
|
*k = kind;
|
||||||
|
}
|
||||||
|
dispatch_panel_event(
|
||||||
|
editor,
|
||||||
|
fid,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
semantic_states,
|
||||||
|
&mut render,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// G5d — a release with nothing armed is INERT.
|
||||||
|
send(
|
||||||
|
pmacs_protocol::MouseKind::Up(pmacs_protocol::MouseButton::Left),
|
||||||
|
&mut semantic_states,
|
||||||
|
&mut editor,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!semantic_states[&fid].has_accepted_gesture(),
|
||||||
|
"a stale release terminates nothing, because nothing began"
|
||||||
|
);
|
||||||
|
|
||||||
|
// G5g — a RIGHT press does not arm.
|
||||||
|
send(
|
||||||
|
pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Right),
|
||||||
|
&mut semantic_states,
|
||||||
|
&mut editor,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!semantic_states[&fid].has_accepted_gesture(),
|
||||||
|
"a right press opens a menu and ends there — arming would \
|
||||||
|
manufacture a delayed release at the next authority loss"
|
||||||
|
);
|
||||||
|
|
||||||
|
// G5g — nor does a wheel step.
|
||||||
|
send(
|
||||||
|
pmacs_protocol::MouseKind::ScrollDown,
|
||||||
|
&mut semantic_states,
|
||||||
|
&mut editor,
|
||||||
|
);
|
||||||
|
assert!(!semantic_states[&fid].has_accepted_gesture());
|
||||||
|
|
||||||
|
// Arming, then G5c — an ordinary release CONSUMES.
|
||||||
|
send(
|
||||||
|
pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left),
|
||||||
|
&mut semantic_states,
|
||||||
|
&mut editor,
|
||||||
|
);
|
||||||
|
assert!(semantic_states[&fid].has_accepted_gesture());
|
||||||
|
send(
|
||||||
|
pmacs_protocol::MouseKind::Up(pmacs_protocol::MouseButton::Left),
|
||||||
|
&mut semantic_states,
|
||||||
|
&mut editor,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!semantic_states[&fid].has_accepted_gesture(),
|
||||||
|
"leaving it armed lets a later invalidation duplicate the \
|
||||||
|
release for a button already up"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
semantic_states[&fid].panel_gesture_cancellations(),
|
||||||
|
0,
|
||||||
|
"an ordinary release is not a cancellation — counting it \
|
||||||
|
would make replay deliver a release for a button the user \
|
||||||
|
already lifted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// §5b — SUBSTRATE for the framing's G5p: per-frontend gesture
|
||||||
|
/// ownership.
|
||||||
|
///
|
||||||
|
/// A and B hold gestures on their own panels; B losing authority
|
||||||
|
/// cancels B exactly once and leaves A's gesture intact. One global
|
||||||
|
/// latch would make either frontend's lifecycle cancel or erase the
|
||||||
|
/// other's.
|
||||||
|
///
|
||||||
|
/// G5p itself is replay's per §5b's split table — it requires A's
|
||||||
|
/// next valid Drag to still APPLY, which needs replay. The
|
||||||
|
/// ownership shape underneath it is decided here, by putting the
|
||||||
|
/// latch on `SemanticRenderState` rather than beside the dispatcher,
|
||||||
|
/// so it is pinned here.
|
||||||
|
#[test]
|
||||||
|
fn g5_substrate_one_frontends_authority_loss_leaves_anothers_gesture_alone() {
|
||||||
|
let fid_a = FrontendId(772);
|
||||||
|
let fid_b = FrontendId(773);
|
||||||
|
let (mut editor_a, mut states_a, mut render_a, _doc_a, panel_a, epochs_a) =
|
||||||
|
panel_session_at(PROTOCOL_VERSION, fid_a);
|
||||||
|
let buffer_a = editor_a.core.borrow().windows[&panel_a].buffer_id;
|
||||||
|
let generation_a = stamped_generation(&states_a, fid_a);
|
||||||
|
|
||||||
|
// B lives in the same projection map, so a global latch would be
|
||||||
|
// shared between them.
|
||||||
|
states_a.insert(
|
||||||
|
fid_b,
|
||||||
|
crate::semantic_render::SemanticRenderState::for_peer(fid_b, PROTOCOL_VERSION),
|
||||||
|
);
|
||||||
|
states_a.get_mut(&fid_b).expect("B").arm_accepted_gesture(
|
||||||
|
crate::semantic_render::AcceptedPanelGesture {
|
||||||
|
button: pmacs_protocol::MouseButton::Left,
|
||||||
|
coord: pmacs_protocol::CellCoord::new(0, 0),
|
||||||
|
buffer_id: buffer_a,
|
||||||
|
reached_child: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch_panel_event(
|
||||||
|
&mut editor_a,
|
||||||
|
fid_a,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
&mut states_a,
|
||||||
|
&mut render_a,
|
||||||
|
mapped_pointer(fid_a, epochs_a, buffer_a, generation_a),
|
||||||
|
);
|
||||||
|
assert!(states_a[&fid_a].has_accepted_gesture(), "A is armed");
|
||||||
|
assert!(states_a[&fid_b].has_accepted_gesture(), "B is armed");
|
||||||
|
|
||||||
|
// B loses authority.
|
||||||
|
states_a
|
||||||
|
.get_mut(&fid_b)
|
||||||
|
.expect("B")
|
||||||
|
.cancel_accepted_gesture();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!states_a[&fid_b].has_accepted_gesture(),
|
||||||
|
"B's own gesture ends"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
states_a[&fid_b].panel_gesture_cancellations(),
|
||||||
|
1,
|
||||||
|
"exactly once"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
states_a[&fid_a].has_accepted_gesture(),
|
||||||
|
"and A's survives — a global latch would have erased it"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
states_a[&fid_a].panel_gesture_cancellations(),
|
||||||
|
0,
|
||||||
|
"with no cancellation attributed to A"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Criterion 50: a gesture from a source whose latest declaration is
|
/// Criterion 50: a gesture from a source whose latest declaration is
|
||||||
/// not a visible `Present` is dropped.
|
/// not a visible `Present` is dropped.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,29 @@ fn minibuffer_window<T: Clone>(candidates: &[T], selected: Option<usize>) -> (Ve
|
||||||
(window, selected_in_window)
|
(window, selected_in_window)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// §5b G5 — what an accepted panel `Down` established, and what a
|
||||||
|
/// cancellation must therefore terminate.
|
||||||
|
///
|
||||||
|
/// A release has to MATCH the press it ends: same button, same
|
||||||
|
/// encoding, at a coordinate the child was actually told about. A
|
||||||
|
/// cancellation that guessed any of those would put bytes in a child's
|
||||||
|
/// input stream describing an event that never happened.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct AcceptedPanelGesture {
|
||||||
|
/// The button the accepted `Down` carried.
|
||||||
|
pub button: pmacs_protocol::MouseButton,
|
||||||
|
/// The last coordinate known valid for this gesture — where a
|
||||||
|
/// release is delivered when the pointer has since left content.
|
||||||
|
pub coord: pmacs_protocol::CellCoord,
|
||||||
|
/// The panel buffer the gesture belongs to.
|
||||||
|
pub buffer_id: BufferId,
|
||||||
|
/// Whether the press actually REACHED the child, for a reporting
|
||||||
|
/// terminal. A release is owed only if a press was delivered;
|
||||||
|
/// synthesising one for a press the child never saw is the same
|
||||||
|
/// defect in the other direction.
|
||||||
|
pub reached_child: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Owns one `semantic_render` session's projection state: the last
|
/// Owns one `semantic_render` session's projection state: the last
|
||||||
/// viewport the frontend declared, and the diff baseline per buffer
|
/// viewport the frontend declared, and the diff baseline per buffer
|
||||||
/// for the `StyleSpans` and `Decorations` families.
|
/// for the `StyleSpans` and `Decorations` families.
|
||||||
|
|
@ -406,6 +429,31 @@ pub struct SemanticRenderState {
|
||||||
/// `generation` starts at 0 meaning "never established"; the first
|
/// `generation` starts at 0 meaning "never established"; the first
|
||||||
/// real mapping takes 1, because zero is invalid on the wire.
|
/// real mapping takes 1, because zero is invalid on the wire.
|
||||||
panel_mapping: Option<(crate::editor::PanelMappingSnapshot, u64)>,
|
panel_mapping: Option<(crate::editor::PanelMappingSnapshot, u64)>,
|
||||||
|
/// §5b G5 — the panel gesture this frontend has an ACCEPTED `Down`
|
||||||
|
/// for, if any.
|
||||||
|
///
|
||||||
|
/// Per frontend, never global: two frontends can hold gestures on
|
||||||
|
/// distinct panels at once, and one global slot would make either
|
||||||
|
/// one's authority loss cancel or erase the other's (G5p).
|
||||||
|
///
|
||||||
|
/// Cancellation needs this to exist at all. Without a record of
|
||||||
|
/// what was accepted there is nothing to terminate: a release must
|
||||||
|
/// match the press it ends, and a stale `Up` with no accepted
|
||||||
|
/// `Down` must be inert rather than synthesising one.
|
||||||
|
accepted_gesture: Option<AcceptedPanelGesture>,
|
||||||
|
/// §5b — how many armed gestures an authority loss has ended this
|
||||||
|
/// session.
|
||||||
|
///
|
||||||
|
/// A COUNT, not a queue of records. The records are what replay
|
||||||
|
/// consumes to deliver each release, and replay is the branch that
|
||||||
|
/// introduces their reader — a queue landed here would grow one
|
||||||
|
/// entry per cancelled drag for the life of the daemon with nothing
|
||||||
|
/// ever draining it. A saturating count is bounded and still
|
||||||
|
/// distinguishes the two ways a latch empties: an ordinary `Up`
|
||||||
|
/// consumes it and leaves this alone, an authority loss ends it and
|
||||||
|
/// bumps it. It also does not collapse two losses in one dispatcher
|
||||||
|
/// burst the way a boolean flag would.
|
||||||
|
panel_gesture_cancellations: u64,
|
||||||
/// Highest presentation epoch allocated for this session; `0` means
|
/// Highest presentation epoch allocated for this session; `0` means
|
||||||
/// none has been. Advanced only when a frame is actually shipped, so
|
/// none has been. Advanced only when a frame is actually shipped, so
|
||||||
/// a frame that fails validation does not burn an identity the peer
|
/// a frame that fails validation does not burn an identity the peer
|
||||||
|
|
@ -613,6 +661,8 @@ impl SemanticRenderState {
|
||||||
// frame from shipping a redundant authoritative `Absent`.
|
// frame from shipping a redundant authoritative `Absent`.
|
||||||
last_panel_payload: Some(PanelFramePayload::Absent),
|
last_panel_payload: Some(PanelFramePayload::Absent),
|
||||||
panel_mapping: None,
|
panel_mapping: None,
|
||||||
|
accepted_gesture: None,
|
||||||
|
panel_gesture_cancellations: 0,
|
||||||
panel_epoch_used: 0,
|
panel_epoch_used: 0,
|
||||||
panel_presentation: None,
|
panel_presentation: None,
|
||||||
panel_error_latched: false,
|
panel_error_latched: false,
|
||||||
|
|
@ -644,6 +694,69 @@ impl SemanticRenderState {
|
||||||
self.last_panel_payload.clone()
|
self.last_panel_payload.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// §5b — how many armed gestures an authority loss has ended.
|
||||||
|
///
|
||||||
|
/// Counts rather than collapses: two losses in one dispatcher burst
|
||||||
|
/// read as two. Coincident causes on ONE transition are a different
|
||||||
|
/// matter — those take the latch once, so the count moves once.
|
||||||
|
#[must_use]
|
||||||
|
pub fn panel_gesture_cancellations(&self) -> u64 {
|
||||||
|
self.panel_gesture_cancellations
|
||||||
|
}
|
||||||
|
|
||||||
|
/// §5b — arm the latch, and ONLY for an accepted left press.
|
||||||
|
///
|
||||||
|
/// A right press opens a menu and ends there; `Move`, wheel and the
|
||||||
|
/// other buttons begin nothing. Arming on any accepted pointer
|
||||||
|
/// event would let one of them manufacture a delayed release at the
|
||||||
|
/// next authority loss.
|
||||||
|
///
|
||||||
|
/// This is the SUBSTRATE for the framing's G5g. G5g itself — the
|
||||||
|
/// table-driven non-gesture events followed by an authority loss,
|
||||||
|
/// asserted to emit no synthetic release — is `panel-pointer-replay`'s
|
||||||
|
/// per §5b's split table, because the release it denies does not
|
||||||
|
/// exist on this branch.
|
||||||
|
pub fn arm_accepted_gesture(&mut self, gesture: AcceptedPanelGesture) {
|
||||||
|
self.accepted_gesture = Some(gesture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// §5b — an ordinary accepted release CONSUMES the latch, WITHOUT
|
||||||
|
/// counting as a cancellation.
|
||||||
|
///
|
||||||
|
/// Otherwise a later invalidation finds a gesture it believes is
|
||||||
|
/// still live and synthesises a duplicate release for a button
|
||||||
|
/// already up. Substrate for the framing's G5c/G5d, whose duplicate-
|
||||||
|
/// release assertions are replay's.
|
||||||
|
pub fn consume_accepted_gesture(&mut self) -> Option<AcceptedPanelGesture> {
|
||||||
|
self.accepted_gesture.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// §5b G5a — cancellation: end the live gesture, if any, and return
|
||||||
|
/// its record so the caller can terminate it.
|
||||||
|
///
|
||||||
|
/// Returning the record rather than acting is deliberate. The
|
||||||
|
/// EFFECTS — clearing an empty selection, clearing the click chain,
|
||||||
|
/// delivering the child's release — live in panel replay, which is
|
||||||
|
/// `panel-pointer-replay`'s. This is the trigger and the state; the
|
||||||
|
/// two meet at that rebase.
|
||||||
|
///
|
||||||
|
/// Idempotent on an empty latch: no gesture, no count. That is what
|
||||||
|
/// lets a duplicate `Absent` and a coincident advance both call it
|
||||||
|
/// without inventing a second cancellation.
|
||||||
|
pub fn cancel_accepted_gesture(&mut self) -> Option<AcceptedPanelGesture> {
|
||||||
|
let cancelled = self.accepted_gesture.take();
|
||||||
|
if cancelled.is_some() {
|
||||||
|
self.panel_gesture_cancellations = self.panel_gesture_cancellations.saturating_add(1);
|
||||||
|
}
|
||||||
|
cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a gesture is currently accepted, for assertions.
|
||||||
|
#[must_use]
|
||||||
|
pub fn has_accepted_gesture(&self) -> bool {
|
||||||
|
self.accepted_gesture.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
/// Advance-if-changed, then read: the authoritative mapping key.
|
/// Advance-if-changed, then read: the authoritative mapping key.
|
||||||
///
|
///
|
||||||
/// **The single seam §5b requires.** Projection stamps the frame
|
/// **The single seam §5b requires.** Projection stamps the frame
|
||||||
|
|
@ -660,12 +773,27 @@ impl SemanticRenderState {
|
||||||
snapshot: Option<crate::editor::PanelMappingSnapshot>,
|
snapshot: Option<crate::editor::PanelMappingSnapshot>,
|
||||||
) -> Option<u64> {
|
) -> Option<u64> {
|
||||||
let snapshot = snapshot?;
|
let snapshot = snapshot?;
|
||||||
let next = match &self.panel_mapping {
|
// Matched by value, not by reference: the changed arm CANCELS,
|
||||||
// Compared STRUCTURALLY. A hash would make this
|
// and cancelling needs `&mut self`.
|
||||||
|
let seen = self
|
||||||
|
.panel_mapping
|
||||||
|
.as_ref()
|
||||||
|
.map(|(seen, generation)| (*seen == snapshot, *generation));
|
||||||
|
let next = match seen {
|
||||||
|
// Compared STRUCTURALLY above. A hash would make this
|
||||||
// probabilistic, and a collision here silently accepts a
|
// probabilistic, and a collision here silently accepts a
|
||||||
// stale gesture — the exact failure the key exists for.
|
// stale gesture — the exact failure the key exists for.
|
||||||
Some((seen, generation)) if *seen == snapshot => *generation,
|
Some((true, generation)) => generation,
|
||||||
Some((_, generation)) => generation.saturating_add(1),
|
Some((false, generation)) => {
|
||||||
|
// §5b G5a — the key advancing IS the cancellation
|
||||||
|
// trigger, raised here rather than waiting for another
|
||||||
|
// pointer event. Reactive cancellation loses a race: if
|
||||||
|
// the successor frame reaches the frontend before the
|
||||||
|
// physical `Up`, the producer clears its latch and the
|
||||||
|
// cancelling event never arrives.
|
||||||
|
self.cancel_accepted_gesture();
|
||||||
|
generation.saturating_add(1)
|
||||||
|
}
|
||||||
// First establishment takes 1, never 0: zero is the wire's
|
// First establishment takes 1, never 0: zero is the wire's
|
||||||
// "uninitialised" value and is refused on sight.
|
// "uninitialised" value and is refused on sight.
|
||||||
None => 1,
|
None => 1,
|
||||||
|
|
@ -1542,6 +1670,22 @@ impl SemanticRenderState {
|
||||||
/// deliberately survives: it is answered by the frontend, not by the
|
/// deliberately survives: it is answered by the frontend, not by the
|
||||||
/// panel's presence.
|
/// panel's presence.
|
||||||
fn publish_absent_panel(&mut self, out: &mut Vec<InstanceMessage>) {
|
fn publish_absent_panel(&mut self, out: &mut Vec<InstanceMessage>) {
|
||||||
|
// §5b — `Absent` is a loss of gesture authority: it clears
|
||||||
|
// `panel_presentation` two lines below, so every later inbound
|
||||||
|
// event for that gesture is refused and its release can never
|
||||||
|
// arrive. Cancelled even on a DUPLICATE `Absent`, which does no
|
||||||
|
// wire work but must still leave no live gesture behind — the
|
||||||
|
// clears below are idempotent for the same reason.
|
||||||
|
//
|
||||||
|
// Only the producer half lands here. The framing's G5b matrix —
|
||||||
|
// the panel-epoch, buffer-replacement, same-size geometry and
|
||||||
|
// detach transitions, each with its v24 and v25 legs and its
|
||||||
|
// document and terminal effects — is `panel-pointer-replay`'s per
|
||||||
|
// §5b's split table. Those transitions leave the latch armed on
|
||||||
|
// this branch, which is inert here (nothing consumes it) and
|
||||||
|
// becomes a defect only once replay gives it effects, in the
|
||||||
|
// branch that owns the row.
|
||||||
|
self.cancel_accepted_gesture();
|
||||||
self.panel_presentation = None;
|
self.panel_presentation = None;
|
||||||
// `Absent` also clears the peer's retained mode line. A later
|
// `Absent` also clears the peer's retained mode line. A later
|
||||||
// `Present` under `NoMessage` therefore has nothing it can
|
// `Present` under `NoMessage` therefore has nothing it can
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue