Merge pull request #243 from levineuwirth/panel-pointer-replay
Panel-pointer replay (parent acceptance 48)
This commit is contained in:
commit
6c9bae6948
|
|
@ -270,6 +270,362 @@ hazard in a shape that looks committed. **A documented error message
|
||||||
that never appears is worse than no documentation**, because the reader
|
that never appears is worse than no documentation**, because the reader
|
||||||
waits for a signal that is not coming.
|
waits for a signal that is not coming.
|
||||||
|
|
||||||
|
## Panel-pointer replay (parent acceptance 48) — ACTIVE, 1b's prerequisite
|
||||||
|
|
||||||
|
**Written with the branch's FIRST commit**, per the standing correction
|
||||||
|
from #171 and #215 — the correction the 1b lane missed, honoured here.
|
||||||
|
|
||||||
|
- **Branch `panel-pointer-replay`**, base `githubsucks/main` @
|
||||||
|
**`72da24a`** exactly, in worktree
|
||||||
|
`/home/jeans/Repos/personal/pmacs-panel-replay`.
|
||||||
|
**`githubsucks/panel-pointer-replay` is the authoritative tip** (the
|
||||||
|
ref, not a SHA). Recover with
|
||||||
|
`git fetch githubsucks && git checkout panel-pointer-replay`.
|
||||||
|
- **No PR yet. Checkpoint: framing revision 16 APPROVED;
|
||||||
|
IMPLEMENTATION UNDER WAY.** Read the tip with
|
||||||
|
`git log --oneline githubsucks/main..HEAD`; no count or SHA is
|
||||||
|
recorded here, for the reason the §5b lane learned twice.
|
||||||
|
- **LANDED: Q#BP-R4's pre-effect disposition and the lifecycle
|
||||||
|
table** — `PanelPointerOutcome` (`Refused`/`Consumed`/`Accepted`)
|
||||||
|
decided before any target effect, with the resolution carried so
|
||||||
|
the daemon never re-derives.
|
||||||
|
- **LANDED: G5k's recorded gesture domain.** The first version was
|
||||||
|
reviewed and rejected: it drove tails through the mode-sensitive
|
||||||
|
adapter, which re-reads Shift, scroll position and the child's
|
||||||
|
modes per event — G5k's named mutation. The press now records
|
||||||
|
`PanelGestureDomain` (document / terminal-child-with-encoding /
|
||||||
|
terminal-local) and every tail and completion follows it.
|
||||||
|
- **LANDED: the record is SELF-CONTAINED.** `TerminalLocal` carries
|
||||||
|
the accepted content **viewport** (ambient geometry is `None`
|
||||||
|
exactly when a hidden panel needs completing); a press that anchors
|
||||||
|
nothing no longer arms on either target; and pointer routing goes
|
||||||
|
through the renderer's own `terminal_projection_size` clamp — a
|
||||||
|
band wider than `MAX_TERMINAL_COLS` painted fine while every click
|
||||||
|
inside it resolved to nothing.
|
||||||
|
- **LANDED: `PanelPointerDisposition` is an ENUM.** As
|
||||||
|
`{outcome, Option<target>}` the invalid pair — refused, yet
|
||||||
|
carrying a target — stayed representable inside `editor.rs`. Now
|
||||||
|
`Refused` holds no target at all.
|
||||||
|
- **Witnesses: G5k(a)–(d), P1, P2, P3 both legs, P4, P5, P7, P8, P9,
|
||||||
|
P10, P11, P12**, each biting its own mutation, including G5k's
|
||||||
|
verbatim. **They do NOT all read a target effect, and an earlier
|
||||||
|
version of this line said they did.** Two kinds:
|
||||||
|
- **Effect rows** — G5k(a)–(d), P1, P3 both legs, P4, P5, P7, P8,
|
||||||
|
P11, P12 — read the child's byte stream (**exact bytes**, not a
|
||||||
|
count), the terminal drag state, or the document selection.
|
||||||
|
- **Arming-gate rows** — **P9 and P10** — read the LATCH, and that
|
||||||
|
is correct for them: the defect they fence is a record existing
|
||||||
|
for a gesture that never began, so the record IS the artifact.
|
||||||
|
Manufacturing an effect assertion for them would not distinguish
|
||||||
|
their mutations.
|
||||||
|
**P2 is a third case**: its FOCUS assertion is witnessed —
|
||||||
|
removing the buffer check accepts the press, which activates the
|
||||||
|
panel before replaying — and its classification is checked LAST so
|
||||||
|
the row still fails if it stops testing a refusal. Only its
|
||||||
|
controller and byte assertions are defence in depth, because that
|
||||||
|
mutation routes through a document buffer and touches neither. An
|
||||||
|
earlier version asserted the refusal first, which aborted the row
|
||||||
|
before dispatch and made every effect assertion unreachable; I
|
||||||
|
recorded that ordering limit as a limit of the type boundary, and
|
||||||
|
it was not one.
|
||||||
|
- **Every fixture asserts its own precondition** (the disposition is
|
||||||
|
`Accepted`, or is `Refused`) because four rows in these rounds
|
||||||
|
passed vacuously: cells that were out of grid, or that clamped to
|
||||||
|
byte 0, exercised a refusal instead of the path they named.
|
||||||
|
- **LANDED: the pending-release SLOT and its drain order (task 18).**
|
||||||
|
Cancellation parks the record instead of returning it into a
|
||||||
|
context that drops it — two of the three cancellation sites are
|
||||||
|
inside frame production, where no target effect can run. The drain
|
||||||
|
pays it **before any subsequent panel-pointer effect**, **before
|
||||||
|
detach teardown**, and **at the projection seam** between
|
||||||
|
`render_frame` returning and its messages being written.
|
||||||
|
- **A LIVE gesture is ended and PAID before a replacement press
|
||||||
|
lands.** The entry drain alone was not enough: it looks for an
|
||||||
|
OWED release, and a live gesture owes nothing yet — arming was
|
||||||
|
what cancelled it, which happens after the replacement has
|
||||||
|
already reached the target. The child saw `old press, new press,
|
||||||
|
old release`.
|
||||||
|
- **Witnessed: Q1, Q2, Q3, Q4, Q6.** Q3 asserts ORDER, not arrival.
|
||||||
|
Q6 sends a second press with the first still live and expects
|
||||||
|
exactly `old release, new press` in the child's stream. Both
|
||||||
|
layers of Q6 are proven separately: the invariant now asserts at
|
||||||
|
the point of ARMING (not inside cancellation) and fires in debug,
|
||||||
|
and with that assert compiled out the byte-order assertion
|
||||||
|
catches the same defect — which is what a release build relies
|
||||||
|
on.
|
||||||
|
- **Q5 is CLOSED.** The projection seam is extracted as
|
||||||
|
`project_semantic_frame`, which returns its messages **unwritten**
|
||||||
|
— so a caller holding them has by construction not sent the
|
||||||
|
successor frame, and a release already delivered at that moment
|
||||||
|
provably precedes it. The row bites its own drain and no other:
|
||||||
|
removing it fails Q5 while Q1–Q4 stay green on the other two
|
||||||
|
drain points.
|
||||||
|
- **LANDED: the authority-loss matrix (task 19).** §5b wired `Absent`
|
||||||
|
and left the other four transitions armed — inert while nothing
|
||||||
|
consumed the latch, defects the moment cancellation gained an
|
||||||
|
effect. Three are visible in the producer where the declaration is
|
||||||
|
built (**window replacement**, **buffer replacement**, and a
|
||||||
|
**geometry-epoch change including at an unchanged size**, which
|
||||||
|
needed a retained `geometry_epoch` because nothing else the
|
||||||
|
producer holds moves with it); **detach** cancels in the dispatcher
|
||||||
|
before any teardown.
|
||||||
|
- **Witnessed by a TABLE-DRIVEN matrix**: **five** transitions ×
|
||||||
|
two families × two targets, **twenty quadrants** — `Absent`
|
||||||
|
included, both as the fifth row and as a CONTROL on §5b's own
|
||||||
|
cancellation, which a mutation confirms it catches. The count is
|
||||||
|
asserted in the row, because a loop that quietly stops covering a
|
||||||
|
combination passes exactly as loudly as one that covers them all.
|
||||||
|
Each quadrant **drains explicitly and asserts the effect** — the exact release bytes
|
||||||
|
for a reporting terminal, the cleared empty selection for a
|
||||||
|
document, and an empty slot afterwards. An earlier version
|
||||||
|
stopped at `has_pending_release()` and would have passed while
|
||||||
|
delivery was broken; the mutation that parks a release and never
|
||||||
|
delivers it now fails every row.
|
||||||
|
- **Mutation labels, stated correctly this time**: dropping the
|
||||||
|
BUFFER comparison misses the buffer transition; dropping the
|
||||||
|
WINDOW comparison misses the window transition; dropping the
|
||||||
|
geometry comparison misses that one; dropping detach's cancel
|
||||||
|
misses detach. Each fails the matrix.
|
||||||
|
- **G5m takes both composites the framing names** — changed-size
|
||||||
|
geometry (epoch **and** mapping generation) and a buffer
|
||||||
|
replacement that also moves the mapping — and reads the child's
|
||||||
|
stream rather than a cancellation count, because a count of one
|
||||||
|
proves the latch was taken once, not that one release went out.
|
||||||
|
It also **asserts the mapping generation actually advances**:
|
||||||
|
without that, a same-size geometry change passes as a
|
||||||
|
"composite" while being a single cause, and the row would prove
|
||||||
|
nothing about coincidence. Peeked rather than read through the
|
||||||
|
authoritative accessor, which would advance the key and
|
||||||
|
manufacture the second cause.
|
||||||
|
- **G5j has two legs and they differ**: an empty selection is
|
||||||
|
cleared without moving point, a REAL dragged region survives
|
||||||
|
anchor-and-cursor exact. Clearing every selection fails the
|
||||||
|
second.
|
||||||
|
- **One quadrant asserts less, and says so**: for window
|
||||||
|
replacement on a document, the window the gesture belonged to is
|
||||||
|
gone, so the completion has nothing left to clear and the
|
||||||
|
gesture ending is the whole effect. Written into the row rather
|
||||||
|
than left as a silently absent assertion.
|
||||||
|
- **GATE RUN 1, head-exact at `6142acc`: 15/16, red on `04-lib-crdt`
|
||||||
|
only.** `composition_overhead_under_ten_percent` (1.247× against
|
||||||
|
1.10×) and `setsid_escapee_is_not_reaped_and_teardown_reclaims_readers`
|
||||||
|
failed together; both green on isolated rerun. Recorded as **U12**.
|
||||||
|
`src/process.rs` is not touched by this branch at all. **The run
|
||||||
|
was knowingly taken on a machine that was quieter but not quiet** —
|
||||||
|
load 11.04 at start, 27.79 five-minute at end, two foreign `python`
|
||||||
|
processes throughout — so it is reported as a red under stated
|
||||||
|
conditions rather than dismissed.
|
||||||
|
- **REMAINING: a head-exact gate on a genuinely quiet machine, then
|
||||||
|
the PR.** The gate
|
||||||
|
wants a QUIET machine: a foreign C++/java build has been running at
|
||||||
|
load 114+ through this work, and the wall-clock rows
|
||||||
|
(`composition_overhead_under_ten_percent`,
|
||||||
|
`m6_2_pty_streaming_respects_byte_ceiling`,
|
||||||
|
`full_buffer_summary_flatten_scales_on_large_grammar_file`) redded
|
||||||
|
under it and were green in isolation every time. That is U6/U9/U10
|
||||||
|
territory and running the gate into it would manufacture another
|
||||||
|
rotating-red incident.
|
||||||
|
- **Two test seams added for this:** an opt-in child-input tap
|
||||||
|
(`start_send_tap_for_test`) and a drag-state read
|
||||||
|
(`view_is_dragging_for_test`). Nothing else exposes what the child
|
||||||
|
actually received, which is what these rows must assert. §5a's **pre-merge** replay contract was approved at
|
||||||
|
revision 12; revisions 14–16 are the post-merge amendment now under
|
||||||
|
review. Revision 13 ruled Q#BP-R3 and blocked the lane on a
|
||||||
|
protocol-bearing mapping generation; **that block is DISCHARGED** —
|
||||||
|
the slice merged as #242 (`47b5463`).
|
||||||
|
- **MERGED main into this branch** at `b758c2e` rather than rebasing:
|
||||||
|
the lane's 12 commits include 10 framing revisions over the same
|
||||||
|
800–1000 line doc regions, so a rebase meant twelve rounds of
|
||||||
|
large-block resolution — the operation that produced a committed
|
||||||
|
diff3 marker on the last lane. Workspace compiles clean, **1964 lib
|
||||||
|
and 284 GPU tests pass**. Base is no longer `72da24a`; read it with
|
||||||
|
`git merge-base githubsucks/main HEAD`.
|
||||||
|
- **THE MERGE CREATED ONE DEFECT AND SURFACED ONE COLLISION.**
|
||||||
|
- **Defect, fixed at `cf78385`: `b758c2e` DOES NOT COMPILE**, and its
|
||||||
|
message claims "Workspace compiles clean". **That claim is
|
||||||
|
withdrawn.** I staged the resolution, hit `cannot find value mods`
|
||||||
|
at the mapped arm, fixed it, re-checked clean — and committed
|
||||||
|
without re-staging, so the verification and the commit were of
|
||||||
|
different trees. Not amended away: `b758c2e` keeps its false claim
|
||||||
|
with the withdrawal attached, because erasing a bad record is worse
|
||||||
|
than carrying a corrected one. **Anything bisecting across
|
||||||
|
`b758c2e`..`3cd7b8a` will fail to build.**
|
||||||
|
- **Defect, fixed:** the merge kept BOTH copies of §5b — this
|
||||||
|
branch's stale pre-split one and main's authoritative one. I
|
||||||
|
discarded the uncommitted stub edit as obsolete and missed that its
|
||||||
|
**deletion** half was still owed. Removed; exactly one §5a and one
|
||||||
|
§5b remain.
|
||||||
|
- **Collision, ruled by revision 14 and NOW FIXED IN CODE** (see the
|
||||||
|
checkpoint above; this bullet records what it was):
|
||||||
|
§5b and this lane gave `dispatch_semantic_panel_pointer`'s `bool`
|
||||||
|
different meanings — accepted-as-a-gesture versus consumed-here. A
|
||||||
|
mode-line press therefore **armed the latch** --- past tense: the
|
||||||
|
fix landed with Q#BP-R4, and P1 pins it.
|
||||||
|
**Q#BP-R4** rules a three-state `PanelPointerOutcome`, classified
|
||||||
|
**before** target effects. Only an `Accepted` `Down(Left)` arms;
|
||||||
|
left `Drag`/`Up` require a live record; an accepted `Up` performs
|
||||||
|
ordinary replay once, a consumed/chrome `Up` performs the recorded
|
||||||
|
completion once, and a refused `Up` preserves the record.
|
||||||
|
- **Revision 16 carries** the rows §5b's split table assigned here, a
|
||||||
|
**pending-release slot** for the cancellation record §5b leaves
|
||||||
|
nowhere to wait, and the **four transitions** that strand a live
|
||||||
|
gesture once effects attach. Drain order is executable: before the
|
||||||
|
next panel-pointer effect; before detach teardown; and, for a
|
||||||
|
projection-raised cancellation, after `render_frame` returns but
|
||||||
|
before any returned message is written. Ground truth is re-measured
|
||||||
|
at `2c0d3ff`. Document-panel horizontal scrolling is a named deferral
|
||||||
|
to GUI Stage 1b B1–B3; a horizontal tick whose terminal precedence
|
||||||
|
selects child reporting already emits SGR, and the local terminal
|
||||||
|
branch has no horizontal viewport effect.
|
||||||
|
- **THE BLOCKER, and why the earlier acceptance failed.** A
|
||||||
|
`PanelPointer` names a cell; nothing on the wire says which inverse
|
||||||
|
mapping the frontend saw, so the daemon inverts against whatever is
|
||||||
|
current. Revision 12 accepted that on three bounds and **all three
|
||||||
|
were wrong**: a **foreign** edit moves the mapping with `view_top`
|
||||||
|
untouched, the error is **unbounded** once ticks/folds/edits/reloads
|
||||||
|
accumulate, and the window lasts until the frontend **presents** the
|
||||||
|
new frame. §5b adds a **cell-mapping generation** — moves with the
|
||||||
|
inverse mapping, stable across focus/styling/cursor/selection-only
|
||||||
|
repaints so drags survive — as **appended** wire variants with
|
||||||
|
bilateral gating.
|
||||||
|
- **Chain: ~~§5b (protocol) →~~ panel replay → GUI arc 1b.** §5b took
|
||||||
|
**v25**, so 1e's `OpenTarget` is **v26**. **That edit is NOT owed by
|
||||||
|
the 1b branch** — this bullet said it was, and said making it here
|
||||||
|
would collide at 1b's rebase. **§5b made it, in
|
||||||
|
`docs/gui-stage1-input-framing.md`, and merged as #242**: a canonical
|
||||||
|
document saying v25 is false the moment v25 is taken, and an expected
|
||||||
|
rebase conflict was not grounds for leaving it wrong. Corrected here
|
||||||
|
because this bullet and the merged-#242 block below it were saying
|
||||||
|
opposite things. Commit one was the ground-truth
|
||||||
|
re-measurement; 6 added the four replay edges; **7 answers review of
|
||||||
|
6; **8 answers review of 7** — R-c is target × gesture-ORIGIN
|
||||||
|
(terminals reject all chrome kinds and raw chrome coords fail the
|
||||||
|
reporting bounds check), R-c2 retains the `Down` cell, A3–A5 add
|
||||||
|
positive SGR controls, and four witness seams are tightened. **New
|
||||||
|
ruling Q#BP-R2**: a chrome wheel over a terminal panel clamps into
|
||||||
|
content rather than dropping — a deliberate divergence from the TUI,
|
||||||
|
flagged for overrule — **and overruled in 9**.
|
||||||
|
**Revision 9** reverses two of 8's decisions: a terminal-chrome wheel
|
||||||
|
is **CONSUMED, not clamped** (SGR wheel input is coordinate-bearing,
|
||||||
|
so clamping fabricates a hit the user never made, and a wheel has no
|
||||||
|
liveness obligation), and the `Down`-cell fallback lives in a
|
||||||
|
**separate `gesture_last_content_cell`** rather than
|
||||||
|
`last_pointer_cell`, which is cleared on press precisely so the first
|
||||||
|
same-cell `Drag` reaches the daemon (`pmacs-gpu/src/main.rs:19841`).
|
||||||
|
**`Up` is the only crossing event promised unconditionally**; a
|
||||||
|
crossing `Drag` is normalized and then deduped.
|
||||||
|
**Revision 10** keeps Q#BP-R2's outcome and moves its **enforcement
|
||||||
|
point**: the GPU **cannot know a panel holds a terminal** —
|
||||||
|
`PanelFrame` has no target-kind field
|
||||||
|
(`pmacs-protocol/src/panel.rs:73`) and `state.terminal` is the
|
||||||
|
primary full-window terminal — so a producer-side rule needed a new
|
||||||
|
wire field this lane must not add. **The producer is target-blind**
|
||||||
|
and sends the chrome wheel for every panel; **the daemon** resolves
|
||||||
|
the side window and decides: document → `scroll_window`, terminal →
|
||||||
|
consume. Witness: one frontend across a document→terminal
|
||||||
|
replacement.
|
||||||
|
**Revision 11** fixes the ORDERING: the terminal-chrome wheel is
|
||||||
|
consumed **before activation**, not merely before
|
||||||
|
`apply_terminal_gesture`. `activates` is `!matches!(kind, Move)` for
|
||||||
|
a terminal (`src/editor.rs:2695`), so the wheel already writes focus
|
||||||
|
and `active_frontend` at `:2699` ahead of any replay decision — a
|
||||||
|
consume check below that would change focus while scrolling nothing
|
||||||
|
and claiming no controller. Four-step order, consumption at step 3;
|
||||||
|
the witness now asserts **focus and controller identity unchanged**.
|
||||||
|
**Revision 12** makes that setup discriminating: leg 2 must **start
|
||||||
|
PASSIVE** — primary document window active, terminal side window
|
||||||
|
distinct and passive, controller baseline captured — because "focus
|
||||||
|
unchanged" is vacuous if the panel is already focused, and the
|
||||||
|
below-activation mutation would then call `focus_window` on the
|
||||||
|
already-active window and pass. The two assertions are **not
|
||||||
|
interchangeable**: **focus** catches the ordering mutation;
|
||||||
|
**controller identity** catches the shared-path mutation, since
|
||||||
|
`apply_terminal_gesture` claims at `src/editor.rs:3571` before local
|
||||||
|
handling and activation alone claims nothing.
|
||||||
|
- **Why this lane exists, re-measured at `2c0d3ff`.** The branch now
|
||||||
|
replays document selection, terminal mouse reporting and vertical
|
||||||
|
wheels; the remaining acceptance-48 effect is **listview row
|
||||||
|
selection**. Q#BP-R4 and §5b's inherited rows still need
|
||||||
|
implementation: pre-effect disposition/latch ordering, fixed-domain
|
||||||
|
gesture tails, exact-once termination, cancellation effects and the
|
||||||
|
pending-release drains. **GUI arc 1b is BLOCKED on this lane and
|
||||||
|
rebases onto its merge commit.**
|
||||||
|
- **No new framing document.** Acceptance 48 is already ruled in
|
||||||
|
`docs/bottom-panel-framing.md`; §5a adds ground truth to it.
|
||||||
|
- **Current clause split.** DONE: click/focus and terminal activation;
|
||||||
|
focused-only auto-scroll with passive `view_top` preserved; lossless
|
||||||
|
and coalesced event delivery; panel document selection; terminal
|
||||||
|
child reporting/local selection; vertical document and terminal
|
||||||
|
wheel effects. MISSING here: listview row selection and the
|
||||||
|
lifecycle/cancellation effects above. Horizontal wheel is split:
|
||||||
|
child-reporting terminal ticks already emit codes 66/67; the local
|
||||||
|
terminal branch is deliberately inert; document-panel `view_left`
|
||||||
|
is explicitly GUI Stage 1b B1–B3's effect, matching the production
|
||||||
|
comment in `src/editor.rs:2999`–`:3003`.
|
||||||
|
- **The scoping hazard.** `set_cursor_byte` (`src/editor_core.rs:1216`),
|
||||||
|
`begin_selection` (`:4691`) and `clear_selection` are
|
||||||
|
**active-window scoped**; used naively they would move the
|
||||||
|
DOCUMENT's point, which AC48 forbids. Activation runs before replay
|
||||||
|
in the same dispatch — **necessary but NOT sufficient**, which is
|
||||||
|
what revision 5 got wrong.
|
||||||
|
- **FOUR REPLAY EDGES (revision 6), each a place a plausible
|
||||||
|
implementation is silently wrong. Three have a precedent in the tree
|
||||||
|
the panel path simply does not use.**
|
||||||
|
- **R-a — modifiers dropped.** The daemon destructures `mods` into
|
||||||
|
`..` (`src/daemon.rs:2425`) and the dispatcher has no modifier
|
||||||
|
parameter, but `apply_terminal_gesture` gates child reporting on
|
||||||
|
`!shift && … && modes.mouse_sgr` (`src/editor.rs:3534`). **Shift is
|
||||||
|
the user's local-selection override**, so a Shift-drag over a
|
||||||
|
reporting terminal panel would send SGR to the child. Thread
|
||||||
|
`mods`; row: Shift-drag selects locally, child receives no bytes.
|
||||||
|
- **R-b — `Drag`/`Up` do not activate**, and another frontend can
|
||||||
|
interleave between them, so replay must not read ambient
|
||||||
|
active-window state. Name a **side-window cell→byte adapter** and a
|
||||||
|
window-TARGETED selection path. `activate_and_position`
|
||||||
|
(`src/editor.rs:3795`) is the precedent *and* the trap: its
|
||||||
|
conversion is window-scoped, but it calls `set_active_window_id`.
|
||||||
|
Rows: interleaved frontend B between A's Down and Drag/Up; orphan
|
||||||
|
Drag/Up on a passive panel leaves the document mirror
|
||||||
|
byte-identical.
|
||||||
|
- **R-c — `panel_grid_size` is the FRAME, not the viewport.** Content
|
||||||
|
is `rows − 1` (`src/editor.rs:2499`–`:2500`) but `panel_hit_test`
|
||||||
|
reports across the whole frame (`pmacs-gpu/src/main.rs:7184`), so a
|
||||||
|
`PanelPointer` can name the **mode-line row**. Terminal viewport is
|
||||||
|
`rows − 1`. **The document rule is PER KIND, not "the row is
|
||||||
|
inert"** — the TUI guards `Down(Left)`/`Drag(Left)`/`Down(Right)`
|
||||||
|
and deliberately not `Up(Left)` or the wheel, so a blanket rule
|
||||||
|
would stop mode-line scrolling and leave a content-started gesture
|
||||||
|
unterminated. **The producer must also not arm** on a mode-line
|
||||||
|
press (`pmacs-gpu/src/main.rs:2878`); a receiver-only rule cannot
|
||||||
|
stop the resulting orphan.
|
||||||
|
- **R-d — replacement leaves the gesture latch armed.** `Absent`
|
||||||
|
clears `pointer_held`/`last_pointer_cell`
|
||||||
|
(`pmacs-gpu/src/main.rs:6909`) but **`Present`→`Present` does not**
|
||||||
|
(`:6913`). A press on A then A→B emits a Drag/release for B with no
|
||||||
|
B press, and **acceptance 49 cannot reject it** — the event carries
|
||||||
|
B's *current* epochs. The **divider** drag latch already
|
||||||
|
epoch-scopes itself (`:7288`); the pointer latch never did. **Both
|
||||||
|
epochs**: a font/scale change advances `geometry_epoch` while
|
||||||
|
`panel_epoch` holds (`pmacs-protocol/src/panel.rs:61`) and clears
|
||||||
|
neither field, so a held gesture resumes under a new grid with
|
||||||
|
valid epochs. **Both fields**: clearing only `pointer_held` leaves
|
||||||
|
the successor's first same-cell `Move` suppressed as a duplicate
|
||||||
|
(`:7238`). Four mutations, including the negative one — an ordinary
|
||||||
|
same-identity refresh must NOT cancel a live gesture.
|
||||||
|
- **RULED: Q#BP-R1** — a single click **SELECTS a listview row only**.
|
||||||
|
RET/SPC remain activation (`listview.lua:610`); no click-to-visit and
|
||||||
|
no double-click-to-visit. Keeps document navigation from becoming an
|
||||||
|
incidental consequence of replay.
|
||||||
|
- **Gates:** the four `bottom_panel_*` acceptance suites plus
|
||||||
|
`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`. **No `--protocol`** —
|
||||||
|
`PanelPointer` and every `MouseKind` it carries already exist on the
|
||||||
|
wire.
|
||||||
|
- **Expected rebase conflict, flagged deliberately:** the `gui-stage1b`
|
||||||
|
branch inserts its own lane at this same position and corrects three
|
||||||
|
stale headers below (#239/#240 still marked OPEN, "1a is next").
|
||||||
|
Those corrections are **left to that branch**; this lane does not
|
||||||
|
duplicate them. The conflict is a normal insertion collision.
|
||||||
|
|
||||||
## Panel cell-mapping generation (v25) — MERGED as #242 (`47b5463`)
|
## Panel cell-mapping generation (v25) — MERGED as #242 (`47b5463`)
|
||||||
|
|
||||||
- **MERGED 2026-08-20T17:25:01Z** at approved head `61f0faf`, merge
|
- **MERGED 2026-08-20T17:25:01Z** at approved head `61f0faf`, merge
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -963,3 +963,31 @@ tree.
|
||||||
the measured `duration_ms` in the assertion message — is **owed its own
|
the measured `duration_ms` in the assertion message — is **owed its own
|
||||||
small lane**. It was deliberately kept out of #242, whose diff does not
|
small lane**. It was deliberately kept out of #242, whose diff does not
|
||||||
touch `src/async_runtime.rs`.
|
touch `src/async_runtime.rs`.
|
||||||
|
|
||||||
|
### U12 — U9's shape in `04-lib-crdt`: a budget test and a PTY test, together
|
||||||
|
|
||||||
|
Recorded during the panel-replay lane's gate, 2026-08-21, on head
|
||||||
|
`6142acc`. **Filed rather than folded into U6 or U9**, because both of
|
||||||
|
those tell it to be: U6 says one of its selectors redding without the
|
||||||
|
other is a different incident, and this is `composition_overhead_under_ten_percent`
|
||||||
|
alone again; U9 is that same shape but in `11-sweep` with a different
|
||||||
|
PTY selector.
|
||||||
|
|
||||||
|
| field | value |
|
||||||
|
| --- | --- |
|
||||||
|
| **selectors** | `--lib --features crdt editor::tests::composition_overhead_under_ten_percent` **and** `process::tests::setsid_escapee_is_not_reaped_and_teardown_reclaims_readers`, failing in the same step |
|
||||||
|
| **job / step** | local (Linux), `scripts/gate` step `04-lib-crdt`, log `20260821T122852Z-2922631` |
|
||||||
|
| **required fragments** | `composition machinery added more than 10% overhead` / `live runtime probe` |
|
||||||
|
| **observed** | **1.247×** against the 1.10× budget (single 192493 ns, dispatch 240130 ns); the PTY row failed at `src/process.rs:5155`, where `active_reader_probe` found no live reader within the 2s `Started` window |
|
||||||
|
| **status** | **one occurrence, both selectors green on isolated rerun** |
|
||||||
|
| **the rest of the run** | **15 of 16 stages green**, including `sweep`, `m4`, `gpu`, `diff-check` and all eight touched acceptance suites |
|
||||||
|
| **what IS established** | both are timing-dependent by construction — one a wall-clock ratio, the other a 2-second liveness window — and each passed alone immediately afterwards. **`src/process.rs` is NOT touched by the observing branch at all**; `src/editor.rs` is, but only in the panel-replay paths, not in composition |
|
||||||
|
| **what is NOT** | that load caused it. Load was **11.04 at the gate's start and 27.79 (5-minute) at its end**, with two foreign `python` processes at ~2 cores throughout and an `apt`/`dpkg` install shortly before. Those are conditions, not a measurement of the mechanism, and the run was **knowingly taken on a machine that was quieter but not quiet** |
|
||||||
|
| **relation to U6** | its selector, alone again, in U6's own step. U6's instruction to judge that separately is honoured for the second time — see U9, which did the same |
|
||||||
|
| **relation to U9** | the same budget-plus-PTY co-failure, in `04-lib-crdt` rather than `11-sweep`, with `setsid_escapee…` where U9 had `m6_1_pty_raw_mode…` |
|
||||||
|
|
||||||
|
**This family has now produced U6, U9, U10 and U12, and the
|
||||||
|
discriminating control U9 named is STILL UNRUN**: pin test-binary
|
||||||
|
concurrency to 1, and separately load a lone `--lib` binary. Four
|
||||||
|
incidents is enough evidence that the family will keep costing review
|
||||||
|
rounds until someone runs it.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
# GUI arc, Stage 1 — input foundation (framing)
|
# GUI arc, Stage 1 — input foundation (framing)
|
||||||
|
|
||||||
**Status: revision 12 — APPROVED.** Revision 12 is §2's ground-truth
|
**Status: revision 13 — AWAITING APPROVAL with bottom-panel §5a
|
||||||
|
revision 16.** Revision 13 changes no previously ruled outcome. It
|
||||||
|
makes B1–B3's “per surface” scope explicit for the panel-document
|
||||||
|
surface, closing the ownership gap exposed by §5a's remeasurement:
|
||||||
|
panel replay is the prerequisite, while GUI 1b owns horizontal tick
|
||||||
|
production, residuals and the document-panel `view_left` effect.
|
||||||
|
|
||||||
|
**Previously, revision 12 — APPROVED.** Revision 12 is §2's ground-truth
|
||||||
re-measurement for Stage 1a and changes no ruling; it carries two
|
re-measurement for Stage 1a and changes no ruling; it carries two
|
||||||
corrections to claims that were wrong at the original anchor too.
|
corrections to claims that were wrong at the original anchor too.
|
||||||
|
|
||||||
|
|
@ -308,9 +315,9 @@ The crate has **exactly one** executable `event_loop.exit()`, in
|
||||||
|
|
||||||
| # | Contract | Witness | Mutation |
|
| # | Contract | Witness | Mutation |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| B1 | Residual per **axis and surface** | deltas discarded | share one accumulator → surface-switch jump |
|
| B1 | Residual per **axis and surface** — primary document, panel document and minimap are distinct owners | deltas discarded | share one accumulator → primary↔panel or document↔minimap surface-switch jump |
|
||||||
| B2 | Wheel-right raises leftmost column; wheel-down raises top line | `x` discarded | invert a sign → that axis's row |
|
| B2 | Wheel-right raises the leftmost column of the **document surface under the pointer**; wheel-down raises its top line. Primary-document horizontal state stays frontend-local; a panel-document tick reaches the daemon's window-targeted `view_left` path | `x` discarded; panel replay currently claims and drops document `ScrollLeft`/`ScrollRight` | invert a sign → that axis's row; omit the panel effect → primary moves while the panel remains inert |
|
||||||
| B3 | Clamps at content bounds; never a negative origin | no horizontal scroll to clamp | remove clamp → **at-bounds row: origin goes negative and the view blanks** |
|
| B3 | Each document surface clamps at its own content bounds; never a negative origin, and wrap pins horizontal origin to zero | no horizontal scroll to clamp | remove a clamp → **at-bounds row: that surface's origin goes negative and the view blanks**; reuse primary bounds for a panel → unequal-width surface row fails |
|
||||||
| B4 | Middle-click paste uses **PRIMARY on Linux** | no middle-click path | use `CLIPBOARD` → B4 only |
|
| B4 | Middle-click paste uses **PRIMARY on Linux** | no middle-click path | use `CLIPBOARD` → B4 only |
|
||||||
| B5 | I-beam over text content only | no I-beam | extend over the gutter → B5 only |
|
| B5 | I-beam over text content only | no I-beam | extend over the gutter → B5 only |
|
||||||
| B6 | Wheel over the minimap scrolls the **document viewport** with **its own residual accumulator**; click/drag remains scrub | **a FULL tick already scrolls today** — minimap pixels are `Elsewhere` (`main.rs:2061`) and the wheel falls through to `scroll_by_lines` (`main.rs:3373`). What fails is **fractional accumulation**, and **residual ownership distinct from the document's**: sub-tick minimap deltas are discarded, and a **surface-switch fractional witness** (part-tick over the minimap, then over the document) must not carry residue across | share the document's accumulator → the surface-switch fractional row jumps |
|
| B6 | Wheel over the minimap scrolls the **document viewport** with **its own residual accumulator**; click/drag remains scrub | **a FULL tick already scrolls today** — minimap pixels are `Elsewhere` (`main.rs:2061`) and the wheel falls through to `scroll_by_lines` (`main.rs:3373`). What fails is **fractional accumulation**, and **residual ownership distinct from the document's**: sub-tick minimap deltas are discarded, and a **surface-switch fractional witness** (part-tick over the minimap, then over the document) must not carry residue across | share the document's accumulator → the surface-switch fractional row jumps |
|
||||||
|
|
@ -322,6 +329,18 @@ blank**. The bound is *width − viewport*, saturating at zero for buffers
|
||||||
narrower than the viewport, and **the right-bound witness asserts the
|
narrower than the viewport, and **the right-bound witness asserts the
|
||||||
final display column is still visible**.
|
final display column is still visible**.
|
||||||
|
|
||||||
|
**Panel-replay consequence, ruled after §5a's 2026-08-20
|
||||||
|
re-measurement.** “Horizontal panel wheel” has three receiver outcomes,
|
||||||
|
not one. When terminal precedence selects child reporting it already
|
||||||
|
forwards `ScrollLeft` and `ScrollRight` as SGR codes 66/67. The local
|
||||||
|
terminal branch — selected by Shift, reporting-off or a scrolled-back
|
||||||
|
view — has no horizontal viewport and remains inert. **B1–B3 own the missing
|
||||||
|
document-panel leg**: fractional production/residuals in the GPU, the
|
||||||
|
lossless `PanelPointer` tick, and the daemon's window-targeted
|
||||||
|
`view_left` effect and clamp. This is a named deferral from panel replay,
|
||||||
|
whose source comment already assigned it here; it does not make the
|
||||||
|
prerequisite lane implement half of 1b early.
|
||||||
|
|
||||||
**Why B6 changed — and revision 5's reason was wrong.** Scrubbing on
|
**Why B6 changed — and revision 5's reason was wrong.** Scrubbing on
|
||||||
wheel is not *impossible*: the wheel handler already reads the cached
|
wheel is not *impossible*: the wheel handler already reads the cached
|
||||||
`state.pointer_pos` for surface routing (`main.rs:3337`), so an absolute
|
`state.pointer_pos` for surface routing (`main.rs:3337`), so an absolute
|
||||||
|
|
|
||||||
|
|
@ -2224,6 +2224,16 @@ struct PanelBand {
|
||||||
/// because the first drag after a press must reach the daemon even at the
|
/// because the first drag after a press must reach the daemon even at the
|
||||||
/// cell the press landed on.
|
/// cell the press landed on.
|
||||||
last_pointer_cell: Option<CellCoord>,
|
last_pointer_cell: Option<CellCoord>,
|
||||||
|
/// Where the live gesture last legitimately pointed **inside content**,
|
||||||
|
/// used to normalize a release that lands on chrome or outside the band
|
||||||
|
/// (parent 48 R-c2).
|
||||||
|
///
|
||||||
|
/// **Deliberately NOT `last_pointer_cell`.** That field is a wire
|
||||||
|
/// DEDUPE BASELINE and is cleared on press precisely so the first drag
|
||||||
|
/// after a press reaches the daemon; storing the press cell there would
|
||||||
|
/// suppress it. This one is a TERMINATION FALLBACK with a different
|
||||||
|
/// lifetime, and `panel_motion_is_new` never consults it.
|
||||||
|
gesture_last_content_cell: Option<CellCoord>,
|
||||||
/// §5b G9b — the mapping generation `last_pointer_cell` was measured
|
/// §5b G9b — the mapping generation `last_pointer_cell` was measured
|
||||||
/// under, so the dedupe re-arms across a mapping change.
|
/// under, so the dedupe re-arms across a mapping change.
|
||||||
///
|
///
|
||||||
|
|
@ -2891,6 +2901,25 @@ impl App {
|
||||||
// so reporting a drag as a hover makes a selection
|
// so reporting a drag as a hover makes a selection
|
||||||
// drag silently do nothing.
|
// drag silently do nothing.
|
||||||
let kind = state.panel_motion_kind();
|
let kind = state.panel_motion_kind();
|
||||||
|
let is_chrome = state.panel_cell_is_chrome(coord);
|
||||||
|
if !is_chrome {
|
||||||
|
let state = self.state.as_mut().expect("checked above");
|
||||||
|
state.panel.gesture_last_content_cell = Some(coord);
|
||||||
|
}
|
||||||
|
let state = self.state.as_mut().expect("checked above");
|
||||||
|
if is_chrome {
|
||||||
|
// A crossing drag is normalized to the last content cell
|
||||||
|
// and then deduped like any other motion — usually
|
||||||
|
// suppressed, because that cell was already reported.
|
||||||
|
let Some(normalized) = state.panel.gesture_last_content_cell else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if state.panel_motion_is_new(normalized) {
|
||||||
|
let mods = translate_mods(self.modifiers);
|
||||||
|
self.send_panel_pointer_at_cell(Some(normalized), kind, mods);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if state.panel_motion_is_new(coord) {
|
if state.panel_motion_is_new(coord) {
|
||||||
let mods = translate_mods(self.modifiers);
|
let mods = translate_mods(self.modifiers);
|
||||||
self.send_panel_pointer_at(x, y, kind, mods);
|
self.send_panel_pointer_at(x, y, kind, mods);
|
||||||
|
|
@ -3013,9 +3042,22 @@ impl App {
|
||||||
// no preceding `Down`, and not arming at all means
|
// no preceding `Down`, and not arming at all means
|
||||||
// `Drag(Left)` is never emitted and panel selection
|
// `Drag(Left)` is never emitted and panel selection
|
||||||
// cannot work at all.
|
// cannot work at all.
|
||||||
if let PointerSurface::PanelCell(_) = panel_surface {
|
if let PointerSurface::PanelCell(coord) = panel_surface {
|
||||||
|
// Parent 48 R-c: a press on the band's MODE LINE is
|
||||||
|
// reserved, and must not arm. Arming would let a drag
|
||||||
|
// into content emit a `Drag` with no accepted `Down` —
|
||||||
|
// an orphan the daemon cannot tell from a real gesture,
|
||||||
|
// and one no receiver-side rule can prevent, because the
|
||||||
|
// frontend has already latched.
|
||||||
|
if state.panel_cell_is_chrome(coord) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let state = self.state.as_mut().expect("checked above");
|
let state = self.state.as_mut().expect("checked above");
|
||||||
state.set_panel_pointer_held(true);
|
state.set_panel_pointer_held(true);
|
||||||
|
// R-c2: the TERMINATION FALLBACK, not the dedupe
|
||||||
|
// baseline. `set_panel_pointer_held` just cleared the
|
||||||
|
// latter on purpose.
|
||||||
|
state.panel.gesture_last_content_cell = Some(coord);
|
||||||
self.send_panel_pointer_at(
|
self.send_panel_pointer_at(
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
|
@ -4481,6 +4523,116 @@ mod input_routing_tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parent 48 R-c, the PRODUCER half — a press on the band's mode line
|
||||||
|
/// must neither send nor arm, and the row above it must do both.
|
||||||
|
///
|
||||||
|
/// This drives the real `MouseInput` path and reads the wire, because
|
||||||
|
/// the hazard is precisely that the frontend latches BEFORE the daemon
|
||||||
|
/// can refuse: `panel_hit_test` reports across the whole frame, so a
|
||||||
|
/// chrome press is indistinguishable from a content press to the
|
||||||
|
/// arming code, and once armed, a drag into content emits a `Drag`
|
||||||
|
/// with no accepted `Down`. No receiver-side rule can undo that.
|
||||||
|
///
|
||||||
|
/// The content leg is not decoration: without it, an implementation
|
||||||
|
/// that never arms anywhere passes the chrome half.
|
||||||
|
#[test]
|
||||||
|
fn a_press_on_the_bands_mode_line_neither_sends_nor_arms() {
|
||||||
|
let mut h = EffectHarness::new();
|
||||||
|
let rows = 4;
|
||||||
|
{
|
||||||
|
// Inlined rather than shared: `present_panel` lives in the
|
||||||
|
// other test module. Same shape — wire on, one declaration,
|
||||||
|
// one `Present`.
|
||||||
|
let state = h.app.state.as_mut().expect("harness state");
|
||||||
|
state.set_panel_wire(PANEL_MIN_VERSION);
|
||||||
|
// The harness already declared during setup, so the `Surface`
|
||||||
|
// trigger dedups; reuse the standing declaration rather than
|
||||||
|
// asserting a second one.
|
||||||
|
let geometry_epoch = state
|
||||||
|
.next_geometry_declaration(GeometryTrigger::Surface)
|
||||||
|
.map_or(state.panel.geometry_epoch, |(epoch, _)| epoch);
|
||||||
|
assert_ne!(
|
||||||
|
geometry_epoch, 0,
|
||||||
|
"a declaration must exist to present against"
|
||||||
|
);
|
||||||
|
let cols = state.declared_cell_total().0.cols.max(1);
|
||||||
|
let frame = pmacs_protocol::panel::PanelFrame {
|
||||||
|
buffer_id: BufferId::from_raw(77),
|
||||||
|
panel_epoch: 1,
|
||||||
|
geometry_epoch,
|
||||||
|
size: CellSize::new(rows, cols),
|
||||||
|
cells: vec![pmacs_protocol::Cell::default(); (rows * cols) as usize],
|
||||||
|
cursor: None,
|
||||||
|
focused: true,
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
state.apply_panel_payload(pmacs_protocol::panel::PanelFramePayload::Present(frame)),
|
||||||
|
"installing a first frame changes the band"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let (ox, oy, _, band_h) = h
|
||||||
|
.app
|
||||||
|
.state
|
||||||
|
.as_ref()
|
||||||
|
.expect("harness state")
|
||||||
|
.panel_content_rect()
|
||||||
|
.expect("a presented band has a rect");
|
||||||
|
let row_h = band_h / rows as f32;
|
||||||
|
let press_at = |h: &mut EffectHarness, y: f32| {
|
||||||
|
h.feed(&WindowEvent::CursorMoved {
|
||||||
|
device_id: DeviceId::dummy(),
|
||||||
|
position: PhysicalPosition::new(f64::from(ox) + 0.5, f64::from(y)),
|
||||||
|
});
|
||||||
|
h.feed(&WindowEvent::MouseInput {
|
||||||
|
device_id: DeviceId::dummy(),
|
||||||
|
state: ElementState::Pressed,
|
||||||
|
button: MouseButton::Left,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let panel_events = |step: &Step| {
|
||||||
|
step.outbound
|
||||||
|
.iter()
|
||||||
|
.filter(|event| matches!(event, pmacs_protocol::FrontendEvent::PanelPointer { .. }))
|
||||||
|
.count()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chrome: the band's LAST row.
|
||||||
|
let step = press_at(&mut h, oy + band_h - 0.5);
|
||||||
|
assert_eq!(
|
||||||
|
panel_events(&step),
|
||||||
|
0,
|
||||||
|
"a press on the mode line is reserved and reaches no daemon"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!h.app
|
||||||
|
.state
|
||||||
|
.as_ref()
|
||||||
|
.expect("harness state")
|
||||||
|
.panel
|
||||||
|
.pointer_held,
|
||||||
|
"and it must not ARM — an armed chrome press turns the next \
|
||||||
|
motion into a `Drag` with no accepted `Down`"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Content: one row up, same column, same gesture.
|
||||||
|
let step = press_at(&mut h, oy + band_h - row_h - 0.5);
|
||||||
|
assert_eq!(
|
||||||
|
panel_events(&step),
|
||||||
|
1,
|
||||||
|
"the row above chrome is content and must still work — without \
|
||||||
|
this leg, never arming anywhere passes the half above"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.app
|
||||||
|
.state
|
||||||
|
.as_ref()
|
||||||
|
.expect("harness state")
|
||||||
|
.panel
|
||||||
|
.pointer_held,
|
||||||
|
"a content press arms the gesture"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// P2, pointer — **the row that shows a route cannot stand in for an
|
/// P2, pointer — **the row that shows a route cannot stand in for an
|
||||||
/// effect.** A wheel carries only a delta; whether it becomes a
|
/// effect.** A wheel carries only a delta; whether it becomes a
|
||||||
/// viewport update, a panel event, a terminal event or nothing at
|
/// viewport update, a panel event, a terminal event or nothing at
|
||||||
|
|
@ -7053,6 +7205,7 @@ impl State {
|
||||||
self.panel.hover_divider = false;
|
self.panel.hover_divider = false;
|
||||||
self.panel.pointer_held = false;
|
self.panel.pointer_held = false;
|
||||||
self.panel.last_pointer_cell = None;
|
self.panel.last_pointer_cell = None;
|
||||||
|
self.panel.gesture_last_content_cell = None;
|
||||||
self.panel.last_pointer_generation = None;
|
self.panel.last_pointer_generation = None;
|
||||||
had
|
had
|
||||||
}
|
}
|
||||||
|
|
@ -7122,6 +7275,35 @@ impl State {
|
||||||
// A duplicate does no work — not even a reshape.
|
// A duplicate does no work — not even a reshape.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Parent 48 R-d: a held gesture belongs to the presentation
|
||||||
|
// it began on. `Absent` clears the latch, but a
|
||||||
|
// `Present` → `Present` REPLACEMENT did not, so a press on
|
||||||
|
// panel A could emit a drag or release for B with no B
|
||||||
|
// press — and acceptance 49 cannot reject that, because the
|
||||||
|
// event carries B's CURRENT epochs. Geometry counts too: a
|
||||||
|
// font or scale change moves `geometry_epoch` while
|
||||||
|
// `panel_epoch` holds, and the gesture would resume under a
|
||||||
|
// new grid.
|
||||||
|
//
|
||||||
|
// Only on a CHANGE of identity. A panel repaints constantly
|
||||||
|
// during a drag, and resetting on every frame would make
|
||||||
|
// selection impossible.
|
||||||
|
//
|
||||||
|
// Compared against the RETAINED frame, never `presented()`:
|
||||||
|
// that accessor filters on `geometry_epoch == self.panel
|
||||||
|
// .geometry_epoch`, and a geometry change advances the field
|
||||||
|
// FIRST, so by the time the matching frame arrives
|
||||||
|
// `presented()` is already `None` and an `is_some_and`
|
||||||
|
// predicate skips the reset — exactly the case D2 covers.
|
||||||
|
let identity_changed = self.panel.frame.as_ref().is_some_and(|current| {
|
||||||
|
current.panel_epoch != frame.panel_epoch
|
||||||
|
|| current.geometry_epoch != frame.geometry_epoch
|
||||||
|
});
|
||||||
|
if identity_changed {
|
||||||
|
self.panel.pointer_held = false;
|
||||||
|
self.panel.last_pointer_cell = None;
|
||||||
|
self.panel.gesture_last_content_cell = None;
|
||||||
|
}
|
||||||
let plan = TerminalPaintPlan::build_grid(
|
let plan = TerminalPaintPlan::build_grid(
|
||||||
frame.size,
|
frame.size,
|
||||||
&frame.cells,
|
&frame.cells,
|
||||||
|
|
@ -7426,12 +7608,29 @@ impl State {
|
||||||
/// A panel selection drag routinely ends past the band's edge, and
|
/// A panel selection drag routinely ends past the band's edge, and
|
||||||
/// dropping that release leaves the daemon holding a button down forever.
|
/// dropping that release leaves the daemon holding a button down forever.
|
||||||
fn panel_release_cell(&self, x: f32, y: f32) -> Option<CellCoord> {
|
fn panel_release_cell(&self, x: f32, y: f32) -> Option<CellCoord> {
|
||||||
|
// Parent 48 R-c/R-c2: `Up` is the load-bearing crossing event — a
|
||||||
|
// gesture whose release is dropped leaves the daemon holding a
|
||||||
|
// button down forever. It is therefore always sent, and always at
|
||||||
|
// a CONTENT coordinate: chrome and outside-the-band both fall back
|
||||||
|
// to where the gesture last legitimately pointed, which is set at
|
||||||
|
// press time so a release with no intervening motion still has one.
|
||||||
match self.classify_pointer_surface(x, y) {
|
match self.classify_pointer_surface(x, y) {
|
||||||
PointerSurface::PanelCell(coord) => Some(coord),
|
PointerSurface::PanelCell(coord) if !self.panel_cell_is_chrome(coord) => Some(coord),
|
||||||
_ => self.panel.last_pointer_cell,
|
_ => self.panel.gesture_last_content_cell,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether `coord` is the band's MODE-LINE row (parent 48 R-c).
|
||||||
|
///
|
||||||
|
/// The daemon projects panel content as `rows - 1` and paints the last
|
||||||
|
/// row as chrome, but `panel_hit_test` reports across the whole frame,
|
||||||
|
/// so a hit is not automatically a content cell.
|
||||||
|
fn panel_cell_is_chrome(&self, coord: CellCoord) -> bool {
|
||||||
|
self.panel
|
||||||
|
.presented()
|
||||||
|
.is_some_and(|frame| coord.row + 1 >= frame.size.rows)
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a panel motion at `coord` carries anything new, and latch it.
|
/// Whether a panel motion at `coord` carries anything new, and latch it.
|
||||||
///
|
///
|
||||||
/// Sub-cell motion resolves to the same cell and says nothing the daemon
|
/// Sub-cell motion resolves to the same cell and says nothing the daemon
|
||||||
|
|
@ -7459,6 +7658,10 @@ impl State {
|
||||||
fn set_panel_pointer_held(&mut self, held: bool) {
|
fn set_panel_pointer_held(&mut self, held: bool) {
|
||||||
self.panel.pointer_held = held;
|
self.panel.pointer_held = held;
|
||||||
self.panel.last_pointer_cell = None;
|
self.panel.last_pointer_cell = None;
|
||||||
|
// The termination fallback dies with the gesture (parent 48 R-c2).
|
||||||
|
// Safe in both directions: the press path rewrites it immediately
|
||||||
|
// after arming, and a release READS it before this runs.
|
||||||
|
self.panel.gesture_last_content_cell = None;
|
||||||
self.panel.last_pointer_generation = None;
|
self.panel.last_pointer_generation = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20475,6 +20678,198 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parent 48 R-c — the band's MODE-LINE row is chrome, and the
|
||||||
|
/// producer must not arm a gesture there.
|
||||||
|
///
|
||||||
|
/// `panel_hit_test` reports across the whole frame, so a chrome press
|
||||||
|
/// looks exactly like a content press to the arming code. If it armed,
|
||||||
|
/// dragging into content would emit a `Drag` with no accepted `Down` —
|
||||||
|
/// an orphan the daemon cannot distinguish from a real gesture, and one
|
||||||
|
/// no receiver-side rule can prevent, because the latch is already set.
|
||||||
|
#[test]
|
||||||
|
fn a_press_on_the_bands_mode_line_neither_arms_nor_reports_content() {
|
||||||
|
let Some(mut state) = headless_or_skip(800, 600, "alpha") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let frame = present_panel(&mut state, 4);
|
||||||
|
let last = frame.size.rows - 1;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
state.panel_cell_is_chrome(pmacs_protocol::CellCoord::new(last, 0)),
|
||||||
|
"the band's last row is its mode line"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!state.panel_cell_is_chrome(pmacs_protocol::CellCoord::new(last - 1, 0)),
|
||||||
|
"the row above it is content — without this the row proves nothing"
|
||||||
|
);
|
||||||
|
|
||||||
|
// A release that lands on chrome normalizes to the last CONTENT
|
||||||
|
// cell rather than reporting the mode line to the daemon.
|
||||||
|
state.set_panel_pointer_held(true);
|
||||||
|
state.panel.gesture_last_content_cell = Some(pmacs_protocol::CellCoord::new(1, 2));
|
||||||
|
let (ox, oy, _, h) = state
|
||||||
|
.panel_content_rect()
|
||||||
|
.expect("a presented band has a rect");
|
||||||
|
let on_chrome_y = oy + h - 0.5;
|
||||||
|
assert_eq!(
|
||||||
|
state.panel_release_cell(ox + 0.5, on_chrome_y),
|
||||||
|
Some(pmacs_protocol::CellCoord::new(1, 2)),
|
||||||
|
"`Up` is the load-bearing crossing event: it must arrive, and at \
|
||||||
|
a CONTENT coordinate — a chrome row would fail the terminal \
|
||||||
|
reporting bounds check and drop into local handling"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parent 48 R-c2 — a release with NO intervening motion still carries
|
||||||
|
/// a coordinate.
|
||||||
|
///
|
||||||
|
/// The press cell is remembered in `gesture_last_content_cell`, not in
|
||||||
|
/// `last_pointer_cell`: that one is cleared on press precisely so the
|
||||||
|
/// first drag after a press reaches the daemon, and storing the press
|
||||||
|
/// cell there would suppress it.
|
||||||
|
#[test]
|
||||||
|
fn a_release_with_no_intervening_motion_carries_the_press_cell() {
|
||||||
|
let Some(mut state) = headless_or_skip(800, 600, "alpha") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
present_panel(&mut state, 4);
|
||||||
|
let press = pmacs_protocol::CellCoord::new(1, 3);
|
||||||
|
|
||||||
|
state.set_panel_pointer_held(true);
|
||||||
|
state.panel.gesture_last_content_cell = Some(press);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
state.panel.last_pointer_cell, None,
|
||||||
|
"arming clears the DEDUPE baseline — the tested guarantee that \
|
||||||
|
the first drag after a press is not suppressed"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Released outside the band entirely, with NO motion in between —
|
||||||
|
// asserted BEFORE any motion probe below, because a probe would
|
||||||
|
// populate `last_pointer_cell` and let a conflated implementation
|
||||||
|
// pass. (It did, on the first run of this row.)
|
||||||
|
assert_eq!(
|
||||||
|
state.panel_release_cell(0.0, 0.0),
|
||||||
|
Some(press),
|
||||||
|
"without the press cell retained, this release has no coordinate \
|
||||||
|
and the daemon is left holding a button down forever"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Separation, checked after: the dedupe must not consult the
|
||||||
|
// fallback, so a drag at the press cell still reaches the daemon.
|
||||||
|
assert!(
|
||||||
|
state.panel_motion_is_new(press),
|
||||||
|
"the first drag after a press must not be suppressed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parent 48 R-d — a held gesture belongs to the presentation it began
|
||||||
|
/// on, and BOTH identities end it.
|
||||||
|
///
|
||||||
|
/// `Absent` already cleared the latch; a `Present` → `Present`
|
||||||
|
/// replacement did not, and acceptance 49 cannot catch the resulting
|
||||||
|
/// orphan because it carries the successor's CURRENT epochs. Geometry
|
||||||
|
/// counts too: a font or scale change moves `geometry_epoch` while
|
||||||
|
/// `panel_epoch` holds.
|
||||||
|
#[test]
|
||||||
|
fn a_change_of_either_panel_identity_ends_a_held_gesture() {
|
||||||
|
let Some(mut state) = headless_or_skip(800, 600, "alpha") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let frame = present_panel(&mut state, 4);
|
||||||
|
|
||||||
|
let live = pmacs_protocol::CellCoord::new(1, 1);
|
||||||
|
let arm = |state: &mut State| {
|
||||||
|
state.set_panel_pointer_held(true);
|
||||||
|
state.panel.gesture_last_content_cell = Some(live);
|
||||||
|
// SEED THE DEDUPE BASELINE TOO. Arming clears it, so a leg that
|
||||||
|
// only arms leaves `last_pointer_cell` already `None` and its
|
||||||
|
// reset is unconstrained — deleting that line stays green. One
|
||||||
|
// accepted motion is what a real gesture would have produced by
|
||||||
|
// the time a replacement arrives.
|
||||||
|
assert!(
|
||||||
|
state.panel_motion_is_new(live),
|
||||||
|
"the first motion after a press is never a duplicate"
|
||||||
|
);
|
||||||
|
assert_eq!(state.panel.last_pointer_cell, Some(live));
|
||||||
|
};
|
||||||
|
|
||||||
|
// D4, the NEGATIVE leg, first: a CHANGED frame at the SAME identity
|
||||||
|
// must not cancel a live gesture. A panel repaints constantly during
|
||||||
|
// a drag, and a reset-every-frame implementation would make
|
||||||
|
// selection impossible while passing D1 and D2.
|
||||||
|
arm(&mut state);
|
||||||
|
let mut refreshed = frame.clone();
|
||||||
|
refreshed.focused = !frame.focused;
|
||||||
|
assert!(
|
||||||
|
state.apply_panel_payload(PanelFramePayload::Present(refreshed)),
|
||||||
|
"a focus repaint is a real frame, not a suppressed duplicate"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
state.panel.pointer_held,
|
||||||
|
"a same-identity repaint must NOT end the gesture"
|
||||||
|
);
|
||||||
|
assert!(state.panel.gesture_last_content_cell.is_some());
|
||||||
|
|
||||||
|
// D1 — panel identity.
|
||||||
|
arm(&mut state);
|
||||||
|
let mut replaced = frame.clone();
|
||||||
|
replaced.panel_epoch = frame.panel_epoch + 1;
|
||||||
|
assert!(state.apply_panel_payload(PanelFramePayload::Present(replaced)));
|
||||||
|
assert!(
|
||||||
|
!state.panel.pointer_held,
|
||||||
|
"a replacement panel never saw the press"
|
||||||
|
);
|
||||||
|
assert_eq!(state.panel.gesture_last_content_cell, None);
|
||||||
|
assert_eq!(state.panel.last_pointer_cell, None);
|
||||||
|
assert!(
|
||||||
|
state.panel_motion_is_new(live),
|
||||||
|
"the dedupe baseline goes too: the successor's first motion at \
|
||||||
|
the predecessor's cell must reach the daemon, not be suppressed \
|
||||||
|
as a duplicate of a gesture that belonged to another panel"
|
||||||
|
);
|
||||||
|
|
||||||
|
// D2 — geometry identity, with panel identity UNCHANGED. This is the
|
||||||
|
// font/scale case: the gesture would otherwise resume under a new
|
||||||
|
// grid carrying epochs that are current and perfectly valid.
|
||||||
|
//
|
||||||
|
// **Driven through the real declaration path.** Inventing a
|
||||||
|
// higher-epoch frame is not the production sequence: a font or
|
||||||
|
// scale change advances `self.panel.geometry_epoch` FIRST, and only
|
||||||
|
// then does the matching frame arrive. That ordering is what broke
|
||||||
|
// the first implementation — `presented()` filters on the epoch, so
|
||||||
|
// it answers `None` in exactly this window — and a witness that
|
||||||
|
// skips the declaration cannot see it.
|
||||||
|
let base = state.panel.frame.clone().expect("a frame is retained");
|
||||||
|
arm(&mut state);
|
||||||
|
let (next_epoch, total) = state
|
||||||
|
.next_geometry_declaration(GeometryTrigger::Metrics)
|
||||||
|
.expect("a metrics change re-declares geometry");
|
||||||
|
assert_ne!(next_epoch, base.geometry_epoch, "the declaration advanced");
|
||||||
|
assert!(
|
||||||
|
state.panel.presented().is_none(),
|
||||||
|
"and the retained frame no longer answers `presented()` — the \
|
||||||
|
window in which a `presented()`-based reset silently skips"
|
||||||
|
);
|
||||||
|
let mut regeometried = base.clone();
|
||||||
|
regeometried.geometry_epoch = next_epoch;
|
||||||
|
regeometried.size = CellSize::new(base.size.rows, total.cols.max(1));
|
||||||
|
regeometried.cells = vec![
|
||||||
|
pmacs_protocol::Cell::default();
|
||||||
|
(regeometried.size.rows * regeometried.size.cols) as usize
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
regeometried.panel_epoch, base.panel_epoch,
|
||||||
|
"the point of this leg is that PANEL identity holds"
|
||||||
|
);
|
||||||
|
assert!(state.apply_panel_payload(PanelFramePayload::Present(regeometried)));
|
||||||
|
assert!(
|
||||||
|
!state.panel.pointer_held,
|
||||||
|
"a new grid is a new gesture context, even under the same panel"
|
||||||
|
);
|
||||||
|
assert_eq!(state.panel.gesture_last_content_cell, None);
|
||||||
|
}
|
||||||
|
|
||||||
/// F1 — a held left button makes motion a `Drag(Left)`, and the dedupe
|
/// F1 — a held left button makes motion a `Drag(Left)`, and the dedupe
|
||||||
/// re-arms on every press and release.
|
/// re-arms on every press and release.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -20515,7 +20910,13 @@ mod tests {
|
||||||
state.panel_release_cell(ox + 0.5, oy + 0.5),
|
state.panel_release_cell(ox + 0.5, oy + 0.5),
|
||||||
Some(pmacs_protocol::CellCoord::new(0, 0))
|
Some(pmacs_protocol::CellCoord::new(0, 0))
|
||||||
);
|
);
|
||||||
|
// The dedupe baseline and the TERMINATION FALLBACK are separate
|
||||||
|
// fields (parent 48 R-c2): `panel_motion_is_new` owns the first and
|
||||||
|
// a release reads only the second, so drive both exactly as the
|
||||||
|
// production motion path does. Conflating them is what would
|
||||||
|
// suppress the first drag after a press, asserted above.
|
||||||
state.panel_motion_is_new(pmacs_protocol::CellCoord::new(1, 7));
|
state.panel_motion_is_new(pmacs_protocol::CellCoord::new(1, 7));
|
||||||
|
state.panel.gesture_last_content_cell = Some(pmacs_protocol::CellCoord::new(1, 7));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.panel_release_cell(ox + 0.5, 0.0),
|
state.panel_release_cell(ox + 0.5, 0.0),
|
||||||
Some(pmacs_protocol::CellCoord::new(1, 7)),
|
Some(pmacs_protocol::CellCoord::new(1, 7)),
|
||||||
|
|
|
||||||
2382
src/daemon.rs
2382
src/daemon.rs
File diff suppressed because it is too large
Load Diff
675
src/editor.rs
675
src/editor.rs
|
|
@ -408,6 +408,167 @@ impl PanelMappingSnapshot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How an authenticated panel event relates to the authoritative panel
|
||||||
|
/// surface, decided BEFORE any target effect (parent 48 Q#BP-R4).
|
||||||
|
///
|
||||||
|
/// Two branches once agreed on `bool` while disagreeing on its meaning
|
||||||
|
/// — §5b read it as "the gesture was accepted" and drove the
|
||||||
|
/// accepted-gesture latch off it, while panel replay read it as "the
|
||||||
|
/// event was consumed here", chrome swallows included. Merged, a press
|
||||||
|
/// on the band's mode line armed a gesture that never began in content.
|
||||||
|
/// **This type exists so that collision cannot be re-created silently:**
|
||||||
|
/// a two-state answer with the corrected meaning would be right today
|
||||||
|
/// and would let the next author restore the bug without touching a
|
||||||
|
/// test.
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||||
|
pub enum PanelPointerOutcome {
|
||||||
|
/// Not addressable as this panel: no grid, out of grid, no side
|
||||||
|
/// window, or a buffer that is not the one shown there.
|
||||||
|
Refused,
|
||||||
|
/// This panel owns the cell/event, but it is deliberately not a
|
||||||
|
/// content gesture — the chrome claims of Q#BP-R2 and R-c.
|
||||||
|
Consumed,
|
||||||
|
/// A content gesture for the resolved target.
|
||||||
|
Accepted,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The domain a live panel gesture was RESOLVED INTO at its accepted
|
||||||
|
/// press, and which every tail and its completion must follow
|
||||||
|
/// (parent 48 G5k).
|
||||||
|
///
|
||||||
|
/// **Re-deciding this per event is G5k's forbidden mutation.** The
|
||||||
|
/// terminal adapter picks between reporting to the child and handling
|
||||||
|
/// locally by reading Shift, the scrollback position and the child's
|
||||||
|
/// current mouse modes — all three of which can change mid-gesture.
|
||||||
|
/// A press reported to the child followed by a release re-evaluated
|
||||||
|
/// after the child turned reporting off leaves that child holding a
|
||||||
|
/// button down forever; the reverse transition sends a child an `Up`
|
||||||
|
/// for a `Down` it never saw.
|
||||||
|
///
|
||||||
|
/// So the press records the contract and the tails obey it. The
|
||||||
|
/// encoding travels with it for the same reason: the report must be
|
||||||
|
/// framed the way the press was framed, not the way the child would
|
||||||
|
/// ask for it now.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PanelGestureDomain {
|
||||||
|
/// A document panel; every write is addressed to `window`.
|
||||||
|
Document {
|
||||||
|
/// The side window the gesture belongs to.
|
||||||
|
window: WindowId,
|
||||||
|
},
|
||||||
|
/// A terminal panel whose press REACHED THE CHILD.
|
||||||
|
TerminalChild {
|
||||||
|
/// The side window the gesture belongs to.
|
||||||
|
window: WindowId,
|
||||||
|
/// The terminal buffer the child is attached to.
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
/// The modes the press was encoded under, replayed verbatim.
|
||||||
|
modes: crate::terminal::screen::TerminalModes,
|
||||||
|
},
|
||||||
|
/// A terminal panel handled LOCALLY — Shift, reporting off, or a
|
||||||
|
/// scrolled-back view at press time.
|
||||||
|
TerminalLocal {
|
||||||
|
/// The side window the gesture belongs to.
|
||||||
|
window: WindowId,
|
||||||
|
/// The terminal buffer whose local selection is being built.
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
/// The CONTENT viewport the press was accepted against.
|
||||||
|
///
|
||||||
|
/// Recorded, not re-fetched. `panel_grid_size` is `None` while
|
||||||
|
/// the panel is hidden or absent, so a completion that asked
|
||||||
|
/// for it could not finish a drag during exactly the
|
||||||
|
/// cancellations that need finishing — and a size-changing
|
||||||
|
/// cancellation would finish against the SUCCESSOR's geometry,
|
||||||
|
/// putting the selection somewhere the user never dragged.
|
||||||
|
viewport: CellSize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PanelGestureDomain {
|
||||||
|
/// The side window this gesture is addressed to.
|
||||||
|
#[must_use]
|
||||||
|
pub fn window(&self) -> WindowId {
|
||||||
|
match self {
|
||||||
|
Self::Document { window }
|
||||||
|
| Self::TerminalChild { window, .. }
|
||||||
|
| Self::TerminalLocal { window, .. } => *window,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the press reached the child, so a release is OWED to it.
|
||||||
|
#[must_use]
|
||||||
|
pub fn reached_child(&self) -> bool {
|
||||||
|
matches!(self, Self::TerminalChild { .. })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which way the shared terminal adapter routed one gesture.
|
||||||
|
///
|
||||||
|
/// Returned rather than recomputed so a panel press can RECORD the
|
||||||
|
/// route it actually took (G5k). Other callers of the adapter ignore
|
||||||
|
/// it — they have no gesture latch to bind.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum TerminalGestureRoute {
|
||||||
|
/// Nothing ran: the view is gone or has no status at that size.
|
||||||
|
None,
|
||||||
|
/// Reported to the child, under these modes.
|
||||||
|
Child(crate::terminal::screen::TerminalModes),
|
||||||
|
/// Handled locally — scrollback, local selection, or the menu.
|
||||||
|
Local,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A classified panel gesture: the outcome, and the resolution it was
|
||||||
|
/// decided from.
|
||||||
|
///
|
||||||
|
/// The resolution travels with the outcome so that
|
||||||
|
/// `EditorState::apply_panel_pointer` acts on the SAME derivation the
|
||||||
|
/// disposition was decided by. Q#BP-R4 fixes the editor as the only
|
||||||
|
/// authority for that derivation; handing the daemon a bare outcome
|
||||||
|
/// and letting it re-derive chrome or target kind would be the hole
|
||||||
|
/// §5b closed, reopened beside the dispatcher.
|
||||||
|
pub enum PanelPointerDisposition {
|
||||||
|
/// Not addressable as this panel. **Carries no target**, which is
|
||||||
|
/// the point: as two independent fields, `Refused` plus a resolved
|
||||||
|
/// target stayed representable inside this module, and the only
|
||||||
|
/// thing preventing a refused gesture from reaching a target was
|
||||||
|
/// that no code currently built that pair.
|
||||||
|
Refused,
|
||||||
|
/// Claimed by this panel, but deliberately not a content gesture.
|
||||||
|
Consumed(ResolvedPanelTarget),
|
||||||
|
/// A content gesture for the resolved target.
|
||||||
|
Accepted(ResolvedPanelTarget),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PanelPointerDisposition {
|
||||||
|
/// The disposition, for the daemon's lifecycle table.
|
||||||
|
#[must_use]
|
||||||
|
pub fn outcome(&self) -> PanelPointerOutcome {
|
||||||
|
match self {
|
||||||
|
Self::Refused => PanelPointerOutcome::Refused,
|
||||||
|
Self::Consumed(_) => PanelPointerOutcome::Consumed,
|
||||||
|
Self::Accepted(_) => PanelPointerOutcome::Accepted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The panel target a disposition resolved to, derived once.
|
||||||
|
///
|
||||||
|
/// **Public as a type, opaque as a value.** It has to be nameable
|
||||||
|
/// because `PanelPointerDisposition`'s variants carry it, but every
|
||||||
|
/// field stays private: the daemon receives a disposition and hands it
|
||||||
|
/// back, and cannot read the side window, the target kind or the
|
||||||
|
/// content bounds out of it. That is Q#BP-R4's first fixed fact — the
|
||||||
|
/// editor is the only authority for the derivation — enforced by
|
||||||
|
/// visibility rather than by convention.
|
||||||
|
pub struct ResolvedPanelTarget {
|
||||||
|
side: WindowId,
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
is_terminal: bool,
|
||||||
|
/// The grid's rows MINUS the mode line — never the frame (R-c).
|
||||||
|
content_rows: u32,
|
||||||
|
cols: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// What decides the mapping BELOW the geometry, which differs by
|
/// What decides the mapping BELOW the geometry, which differs by
|
||||||
/// target kind.
|
/// target kind.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
|
@ -2769,7 +2930,34 @@ impl EditorState {
|
||||||
.is_some_and(|now| now != current)
|
.is_some_and(|now| now != current)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply an accepted `FrontendEvent::PanelPointer` gesture (Q#BP16).
|
/// The byte behind a panel cell, for P9.
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn panel_cell_byte_for_test(&self, win_id: WindowId, coord: CellCoord) -> Option<u64> {
|
||||||
|
self.panel_cell_byte(win_id, coord)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hide this frontend's panel, for P11.
|
||||||
|
///
|
||||||
|
/// `panel_grid_size` returns `None` once hidden, which is the state
|
||||||
|
/// a recorded completion has to survive.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn hide_panel_for_test(&self, frontend_id: FrontendId) {
|
||||||
|
if let Some(view) = self.core.borrow_mut().views.get_mut(&frontend_id) {
|
||||||
|
view.panel_hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-show this frontend's panel, for the Q rows.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn show_panel_for_test(&self, frontend_id: FrontendId) {
|
||||||
|
if let Some(view) = self.core.borrow_mut().views.get_mut(&frontend_id) {
|
||||||
|
view.panel_hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify an authenticated panel gesture, WITHOUT applying it
|
||||||
|
/// (Q#BP-R4).
|
||||||
///
|
///
|
||||||
/// Steps 2, 5, and 6 of Q#BP16's ladder are re-derived here from the
|
/// Steps 2, 5, and 6 of Q#BP16's ladder are re-derived here from the
|
||||||
/// daemon's own state — a live, non-hidden side window whose current
|
/// daemon's own state — a live, non-hidden side window whose current
|
||||||
|
|
@ -2778,6 +2966,101 @@ impl EditorState {
|
||||||
/// epochs) belong to the caller, because only the session holds the
|
/// epochs) belong to the caller, because only the session holds the
|
||||||
/// declaration the frontend was actually looking at.
|
/// declaration the frontend was actually looking at.
|
||||||
///
|
///
|
||||||
|
/// **The answer is a DISPOSITION, decided before any target
|
||||||
|
/// effect.** Q#BP-R4 fixes two facts and this split exists to hold
|
||||||
|
/// them: the editor is the **only** authority that derives
|
||||||
|
/// `Refused`/`Consumed`/`Accepted`, and the disposition completes
|
||||||
|
/// before a left `Drag`/`Up` reaches a child or a selection. The old
|
||||||
|
/// single function could not honour the second — it validated,
|
||||||
|
/// classified and mutated in one pass, so a tail with no accepted
|
||||||
|
/// press had already landed by the time the daemon consulted the
|
||||||
|
/// latch.
|
||||||
|
///
|
||||||
|
/// The returned value carries the resolution it was decided from, so
|
||||||
|
/// [`Self::apply_panel_pointer`] acts on the same derivation rather
|
||||||
|
/// than repeating it. **A second derivation is the hole §5b closed**;
|
||||||
|
/// reopening it beside the dispatcher would be the same defect in a
|
||||||
|
/// new place.
|
||||||
|
#[must_use]
|
||||||
|
pub fn classify_panel_pointer(
|
||||||
|
&self,
|
||||||
|
frontend_id: FrontendId,
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
coord: CellCoord,
|
||||||
|
kind: pmacs_protocol::MouseKind,
|
||||||
|
) -> PanelPointerDisposition {
|
||||||
|
use pmacs_protocol::MouseKind as PKind;
|
||||||
|
|
||||||
|
let refused = PanelPointerDisposition::Refused;
|
||||||
|
|
||||||
|
let Some(size) = self.core.borrow().panel_grid_size(frontend_id) else {
|
||||||
|
return refused;
|
||||||
|
};
|
||||||
|
if coord.row >= size.rows || coord.col >= size.cols {
|
||||||
|
return refused;
|
||||||
|
}
|
||||||
|
// Parent 48 R-c: the panel's LAST ROW IS ITS MODE LINE. Projection
|
||||||
|
// derives content as `rows - 1` while the frontend hit-tests the
|
||||||
|
// whole frame, so a `PanelPointer` can legitimately name chrome —
|
||||||
|
// and chrome is never a content cell for either target.
|
||||||
|
let content_rows = size.rows.saturating_sub(1);
|
||||||
|
if content_rows == 0 {
|
||||||
|
return refused;
|
||||||
|
}
|
||||||
|
let is_terminal = self.terminal_manager.borrow().is_terminal(buffer_id);
|
||||||
|
let side = {
|
||||||
|
let core = self.core.borrow();
|
||||||
|
let Some(side) = core.side_window_for(frontend_id) else {
|
||||||
|
return refused;
|
||||||
|
};
|
||||||
|
if core.windows.get(&side).map(|window| window.buffer_id) != Some(buffer_id) {
|
||||||
|
return refused;
|
||||||
|
}
|
||||||
|
side
|
||||||
|
};
|
||||||
|
|
||||||
|
let resolved = ResolvedPanelTarget {
|
||||||
|
side,
|
||||||
|
buffer_id,
|
||||||
|
is_terminal,
|
||||||
|
content_rows,
|
||||||
|
cols: size.cols,
|
||||||
|
};
|
||||||
|
let on_chrome = coord.row >= content_rows;
|
||||||
|
let is_wheel = matches!(
|
||||||
|
kind,
|
||||||
|
PKind::ScrollUp | PKind::ScrollDown | PKind::ScrollLeft | PKind::ScrollRight
|
||||||
|
);
|
||||||
|
|
||||||
|
// Everything below is CLAIMED by this panel — the cell is ours —
|
||||||
|
// and the only question left is whether it is a content gesture.
|
||||||
|
let consumed = if on_chrome {
|
||||||
|
// Q#BP-R2, step 3 of the ordering: a terminal panel's chrome
|
||||||
|
// wheel is consumed BEFORE focus, before `active_frontend`,
|
||||||
|
// and before any controller claim. Placing it after
|
||||||
|
// activation would leave the wheel changing focus while
|
||||||
|
// scrolling nothing.
|
||||||
|
//
|
||||||
|
// TUI parity for the rest: a terminal never sees a chrome
|
||||||
|
// coordinate (`dispatch_mouse` rejects every kind above
|
||||||
|
// `inner_rows`). Document chrome mirrors the TUI's PER-KIND
|
||||||
|
// rule — presses and motion are reserved, while `Up` and the
|
||||||
|
// wheel fall through, because an `Up` must still terminate a
|
||||||
|
// gesture begun in content and a chrome wheel still scrolls.
|
||||||
|
let _ = is_wheel;
|
||||||
|
is_terminal || matches!(kind, PKind::Down(_) | PKind::Drag(_) | PKind::Move)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if consumed {
|
||||||
|
PanelPointerDisposition::Consumed(resolved)
|
||||||
|
} else {
|
||||||
|
PanelPointerDisposition::Accepted(resolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a classified panel gesture to its target (Q#BP-R4).
|
||||||
|
///
|
||||||
/// **Activation is not uniform, and Q#BP16 says so explicitly.** A
|
/// **Activation is not uniform, and Q#BP16 says so explicitly.** A
|
||||||
/// **press** focuses any panel — that is click-to-focus, and
|
/// **press** focuses any panel — that is click-to-focus, and
|
||||||
/// `Down(Right)` is the context-menu gesture, so both buttons count.
|
/// `Down(Right)` is the context-menu gesture, so both buttons count.
|
||||||
|
|
@ -2796,47 +3079,342 @@ impl EditorState {
|
||||||
/// Review round 1 (R2-5) found the terminal clause applied to both.
|
/// Review round 1 (R2-5) found the terminal clause applied to both.
|
||||||
/// Bare hover neither focuses nor claims, on either kind.
|
/// Bare hover neither focuses nor claims, on either kind.
|
||||||
///
|
///
|
||||||
/// **Replay is out of scope in Stage 2B-2.** Driving selection,
|
/// **Only `Accepted` reaches a target.** A `Consumed` disposition
|
||||||
/// listview rows, or child SGR reporting is parent acceptance 48,
|
/// returns without effect: the claim was the effect.
|
||||||
/// which needs the GPU band and lands in Stage 2B-3.
|
|
||||||
///
|
///
|
||||||
/// Returns whether the gesture was accepted.
|
/// Returns the [`PanelGestureDomain`] the effect RESOLVED INTO, so
|
||||||
///
|
/// an accepted press can record the contract its tails must follow
|
||||||
/// `#[must_use]` because the accepted-gesture latch is driven off
|
/// (G5k). `None` means nothing ran and nothing may be recorded.
|
||||||
/// this answer, and discarding it silently arms on rejected presses
|
pub fn apply_panel_pointer(
|
||||||
/// and consumes on rejected releases.
|
&mut self,
|
||||||
#[must_use]
|
|
||||||
pub fn dispatch_semantic_panel_pointer(
|
|
||||||
&self,
|
|
||||||
frontend_id: FrontendId,
|
frontend_id: FrontendId,
|
||||||
buffer_id: crate::buffer::BufferId,
|
disposition: &PanelPointerDisposition,
|
||||||
coord: CellCoord,
|
coord: CellCoord,
|
||||||
kind: pmacs_protocol::MouseKind,
|
kind: pmacs_protocol::MouseKind,
|
||||||
) -> bool {
|
mods: pmacs_protocol::Modifiers,
|
||||||
let Some(size) = self.core.borrow().panel_grid_size(frontend_id) else {
|
) -> Option<PanelGestureDomain> {
|
||||||
return false;
|
use pmacs_protocol::MouseKind as PKind;
|
||||||
|
|
||||||
|
// Only `Accepted` carries a target into an effect, and the type
|
||||||
|
// is what enforces it: there is no `Refused` value holding one.
|
||||||
|
let PanelPointerDisposition::Accepted(target) = disposition else {
|
||||||
|
return None;
|
||||||
};
|
};
|
||||||
if coord.row >= size.rows || coord.col >= size.cols {
|
|
||||||
return false;
|
let activates = if target.is_terminal {
|
||||||
}
|
!matches!(kind, PKind::Move)
|
||||||
let is_terminal = self.terminal_manager.borrow().is_terminal(buffer_id);
|
|
||||||
let mut core = self.core.borrow_mut();
|
|
||||||
let Some(side) = core.side_window_for(frontend_id) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if core.windows.get(&side).map(|window| window.buffer_id) != Some(buffer_id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let activates = if is_terminal {
|
|
||||||
!matches!(kind, pmacs_protocol::MouseKind::Move)
|
|
||||||
} else {
|
} else {
|
||||||
matches!(kind, pmacs_protocol::MouseKind::Down(_))
|
matches!(kind, PKind::Down(_))
|
||||||
};
|
};
|
||||||
if activates {
|
if activates {
|
||||||
core.focus_window(frontend_id, side);
|
let mut core = self.core.borrow_mut();
|
||||||
|
core.focus_window(frontend_id, target.side);
|
||||||
core.active_frontend = frontend_id;
|
core.active_frontend = frontend_id;
|
||||||
}
|
}
|
||||||
true
|
|
||||||
|
if target.is_terminal {
|
||||||
|
// The ONE terminal pointer path, shared with the TUI and the
|
||||||
|
// document terminal. The viewport is `content_rows`, never the
|
||||||
|
// full grid: passing the frame would make the mode line a child
|
||||||
|
// cell and put every clamp one row out.
|
||||||
|
// THROUGH THE RENDERER'S OWN CLAMP. A panel deliberately does
|
||||||
|
// not inherit the terminal's per-axis PTY caps (Bet B5'), so
|
||||||
|
// a 4K surface at a small font is legitimately wider than
|
||||||
|
// `MAX_TERMINAL_COLS` — and `view_status_for_size` refuses
|
||||||
|
// anything over that cap. Passing the raw panel width made
|
||||||
|
// every gesture on such a panel resolve to `None`: a click
|
||||||
|
// squarely inside the visible terminal did nothing at all,
|
||||||
|
// while the same panel rendered fine because projection
|
||||||
|
// clamps and pointer routing did not agree.
|
||||||
|
let viewport =
|
||||||
|
terminal_projection_size(CellSize::new(target.content_rows, target.cols));
|
||||||
|
let key = TerminalViewKey::new(frontend_id, target.side, target.buffer_id);
|
||||||
|
return match self.apply_terminal_gesture(
|
||||||
|
key,
|
||||||
|
viewport,
|
||||||
|
coord,
|
||||||
|
kind,
|
||||||
|
mods,
|
||||||
|
(coord.row, coord.col),
|
||||||
|
) {
|
||||||
|
TerminalGestureRoute::None => None,
|
||||||
|
TerminalGestureRoute::Child(modes) => Some(PanelGestureDomain::TerminalChild {
|
||||||
|
window: target.side,
|
||||||
|
buffer_id: target.buffer_id,
|
||||||
|
modes,
|
||||||
|
}),
|
||||||
|
TerminalGestureRoute::Local => Some(PanelGestureDomain::TerminalLocal {
|
||||||
|
window: target.side,
|
||||||
|
buffer_id: target.buffer_id,
|
||||||
|
viewport,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A press that placed no anchor began NOTHING, and arming over
|
||||||
|
// it records a gesture whose completion has nothing to
|
||||||
|
// complete. `panel_cell_byte` is `None` past the end of a short
|
||||||
|
// line and on an empty panel, which is not a rare shape.
|
||||||
|
self.replay_panel_document_gesture(frontend_id, target.side, coord, kind, mods)
|
||||||
|
.then_some(PanelGestureDomain::Document {
|
||||||
|
window: target.side,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replay a tail or a completion in the gesture's RECORDED domain
|
||||||
|
/// (parent 48 G5k).
|
||||||
|
///
|
||||||
|
/// **Nothing here re-decides the domain.** The press resolved it and
|
||||||
|
/// the record carries it, so Shift going down mid-drag, the view
|
||||||
|
/// scrolling back, or the child flipping its mouse modes cannot move
|
||||||
|
/// a live gesture from one target to another. Re-evaluating those
|
||||||
|
/// per event is G5k's forbidden mutation, and it strands a child
|
||||||
|
/// press in one direction and fabricates an unmatched child release
|
||||||
|
/// in the other.
|
||||||
|
///
|
||||||
|
/// **The window is read from the record too.** An earlier version
|
||||||
|
/// re-derived the current side window and returned when it had
|
||||||
|
/// changed — which is exactly the set of transitions the stranding
|
||||||
|
/// rows must terminate, so the completion they need would have been
|
||||||
|
/// the one thing that refused to run.
|
||||||
|
pub fn replay_panel_gesture_in_domain(
|
||||||
|
&mut self,
|
||||||
|
frontend_id: FrontendId,
|
||||||
|
domain: PanelGestureDomain,
|
||||||
|
coord: CellCoord,
|
||||||
|
kind: pmacs_protocol::MouseKind,
|
||||||
|
mods: pmacs_protocol::Modifiers,
|
||||||
|
) {
|
||||||
|
match domain {
|
||||||
|
PanelGestureDomain::Document { window } => {
|
||||||
|
self.replay_panel_document_gesture(frontend_id, window, coord, kind, mods);
|
||||||
|
}
|
||||||
|
PanelGestureDomain::TerminalChild {
|
||||||
|
window,
|
||||||
|
buffer_id,
|
||||||
|
modes,
|
||||||
|
} => {
|
||||||
|
// Encoded under the RECORDED modes, and gated on
|
||||||
|
// nothing: not Shift, not the scroll position, not the
|
||||||
|
// child's current modes. Those are precisely the three
|
||||||
|
// inputs G5k forbids re-reading.
|
||||||
|
let key = TerminalViewKey::new(frontend_id, window, buffer_id);
|
||||||
|
let _ = key;
|
||||||
|
if let Some(bytes) = crate::terminal::input::encode_mouse(kind, coord, mods, modes)
|
||||||
|
{
|
||||||
|
self.send_terminal_bytes(buffer_id, &bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PanelGestureDomain::TerminalLocal {
|
||||||
|
window,
|
||||||
|
buffer_id,
|
||||||
|
viewport,
|
||||||
|
} => {
|
||||||
|
let key = TerminalViewKey::new(frontend_id, window, buffer_id);
|
||||||
|
let mut manager = self.terminal_manager.borrow_mut();
|
||||||
|
match kind {
|
||||||
|
pmacs_protocol::MouseKind::Drag(pmacs_protocol::MouseButton::Left) => {
|
||||||
|
let _ = manager.update_selection(key, viewport, coord);
|
||||||
|
}
|
||||||
|
pmacs_protocol::MouseKind::Up(pmacs_protocol::MouseButton::Left) => {
|
||||||
|
let _ = manager.finish_selection(key, viewport, coord);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deliver the RECORDED completion for a gesture the ordinary path
|
||||||
|
/// did not complete (parent 48 Q#BP-R4).
|
||||||
|
///
|
||||||
|
/// Used when a release is `Consumed` — it landed on chrome, so the
|
||||||
|
/// content path never ran — or when an authority loss cancels a
|
||||||
|
/// live gesture. An `Accepted` release must NOT come here: it
|
||||||
|
/// already performed the ordinary in-content completion, and doing
|
||||||
|
/// both is the duplicate P5 forbids.
|
||||||
|
///
|
||||||
|
/// Delivery goes through the gesture's recorded domain at its last
|
||||||
|
/// valid content cell (R-c2), so the release is framed the way its
|
||||||
|
/// press was.
|
||||||
|
pub fn complete_panel_gesture(
|
||||||
|
&mut self,
|
||||||
|
frontend_id: FrontendId,
|
||||||
|
record: &crate::semantic_render::AcceptedPanelGesture,
|
||||||
|
mods: pmacs_protocol::Modifiers,
|
||||||
|
) {
|
||||||
|
let release = pmacs_protocol::MouseKind::Up(pmacs_protocol::MouseButton::Left);
|
||||||
|
self.replay_panel_gesture_in_domain(
|
||||||
|
frontend_id,
|
||||||
|
record.domain,
|
||||||
|
record.coord,
|
||||||
|
release,
|
||||||
|
mods,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replay one accepted gesture into a DOCUMENT panel (parent 48).
|
||||||
|
///
|
||||||
|
/// Every write here is addressed to `side`, never to the ambient
|
||||||
|
/// active window. `Drag` and `Up` do not activate, and another
|
||||||
|
/// frontend's input can interleave between a `Down` and its tail, so
|
||||||
|
/// a replay reading `active_window_mut()` would act on whatever
|
||||||
|
/// happened to be active at that moment (R-b).
|
||||||
|
/// Returns whether a left press ANCHORED a gesture. Every other
|
||||||
|
/// kind returns `false`: only a press can begin one, so only a
|
||||||
|
/// press has an answer to give.
|
||||||
|
fn replay_panel_document_gesture(
|
||||||
|
&mut self,
|
||||||
|
frontend_id: FrontendId,
|
||||||
|
side: WindowId,
|
||||||
|
coord: CellCoord,
|
||||||
|
kind: pmacs_protocol::MouseKind,
|
||||||
|
mods: pmacs_protocol::Modifiers,
|
||||||
|
) -> bool {
|
||||||
|
use pmacs_protocol::{MouseButton as PButton, MouseKind as PKind};
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
PKind::ScrollUp => self.scroll_window(side, -SCROLL_LINES),
|
||||||
|
PKind::ScrollDown => self.scroll_window(side, SCROLL_LINES),
|
||||||
|
|
||||||
|
PKind::Down(PButton::Left) => {
|
||||||
|
self.core.borrow_mut().break_command_chain(frontend_id);
|
||||||
|
let is_double = self.is_double_click(frontend_id, side, coord);
|
||||||
|
let Some(byte) = self.panel_cell_byte(side, coord) else {
|
||||||
|
// No anchor: nothing began, so nothing may arm.
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let extending = mods.contains(pmacs_protocol::Modifiers::SHIFT);
|
||||||
|
let prev = self.core.borrow().windows[&side].cursor;
|
||||||
|
let keep_anchor = extending
|
||||||
|
&& self
|
||||||
|
.core
|
||||||
|
.borrow()
|
||||||
|
.windows
|
||||||
|
.get(&side)
|
||||||
|
.is_some_and(|w| w.selection.is_some());
|
||||||
|
self.panel_set_cursor(side, byte);
|
||||||
|
if is_double && !extending {
|
||||||
|
// Repeated left `Down`s are a multi-click (Q#BP16), and
|
||||||
|
// the second selects the word — the same rule the TUI
|
||||||
|
// applies, resolved against the SIDE window.
|
||||||
|
// Safe to use the active-window helper HERE and only
|
||||||
|
// here: `Down` activated the side window two statements
|
||||||
|
// ago, synchronously, so active == side. The tail
|
||||||
|
// (`Drag`/`Up`) does not activate and uses the
|
||||||
|
// window-targeted writers instead.
|
||||||
|
if self.core.borrow_mut().select_word_at_cursor() {
|
||||||
|
self.mouse_click = None;
|
||||||
|
// A double-click word selection IS an anchored
|
||||||
|
// gesture: its release still has to complete.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if extending {
|
||||||
|
if !keep_anchor {
|
||||||
|
self.panel_set_selection(side, Some(prev));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.panel_set_selection(side, Some(byte));
|
||||||
|
}
|
||||||
|
self.mouse_click = Some(MouseClickState {
|
||||||
|
frontend_id,
|
||||||
|
window_id: side,
|
||||||
|
cell: coord,
|
||||||
|
at: Instant::now(),
|
||||||
|
});
|
||||||
|
// Anchored. Stated here rather than inferred from
|
||||||
|
// selection state afterwards, which a later change
|
||||||
|
// could stop setting without anyone noticing arming had
|
||||||
|
// gone quiet.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
PKind::Drag(PButton::Left) => {
|
||||||
|
self.mouse_click = None;
|
||||||
|
self.core.borrow_mut().break_command_chain(frontend_id);
|
||||||
|
if let Some(byte) = self.panel_cell_byte(side, coord) {
|
||||||
|
self.panel_set_cursor(side, byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PKind::Up(PButton::Left) => {
|
||||||
|
// A click without a drag leaves an ACTIVE BUT EMPTY
|
||||||
|
// selection whose stale anchor would capture the next
|
||||||
|
// shift-motion, so it is cleared rather than left set.
|
||||||
|
let mut core = self.core.borrow_mut();
|
||||||
|
if let Some(window) = core.windows.get_mut(&side)
|
||||||
|
&& window
|
||||||
|
.selection
|
||||||
|
.is_some_and(|selection| selection.anchor == window.cursor)
|
||||||
|
{
|
||||||
|
window.selection = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PKind::Down(PButton::Right) => {
|
||||||
|
self.core.borrow_mut().break_command_chain(frontend_id);
|
||||||
|
if let Some(byte) = self.panel_cell_byte(side, coord) {
|
||||||
|
self.panel_set_cursor(side, byte);
|
||||||
|
}
|
||||||
|
self.open_context_menu(side, coord.row, coord.col, (coord.row, coord.col));
|
||||||
|
}
|
||||||
|
// Claimed and dropped, for two different reasons kept in one
|
||||||
|
// arm because their bodies are identical: horizontal panel
|
||||||
|
// scrolling belongs to GUI arc Stage 1b's B-rows rather than
|
||||||
|
// parent 48, bare `Move` neither focuses nor claims, and the
|
||||||
|
// remaining buttons have no panel semantics at all.
|
||||||
|
PKind::ScrollLeft
|
||||||
|
| PKind::ScrollRight
|
||||||
|
| PKind::Move
|
||||||
|
| PKind::Down(_)
|
||||||
|
| PKind::Up(_)
|
||||||
|
| PKind::Drag(_) => {}
|
||||||
|
}
|
||||||
|
// Only a left press can anchor, and it returns `true` above.
|
||||||
|
// Every other kind reaching here handled something already live.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Byte under a panel cell, resolved against the SIDE window's own
|
||||||
|
/// `view_top` and fold map.
|
||||||
|
///
|
||||||
|
/// Deliberately not `activate_and_position`: that helper converts
|
||||||
|
/// correctly but also calls `set_active_window_id`, and a panel tail
|
||||||
|
/// must not re-activate a window on a frontend whose active window may
|
||||||
|
/// have moved since the `Down` (R-b).
|
||||||
|
fn panel_cell_byte(&self, win_id: WindowId, coord: CellCoord) -> Option<u64> {
|
||||||
|
let core = self.core.borrow();
|
||||||
|
let window = core.windows.get(&win_id)?;
|
||||||
|
let buffer_id = window.buffer_id;
|
||||||
|
let view_top = window.view_top;
|
||||||
|
let folds = core.fold_map_for_window(win_id);
|
||||||
|
let display_row = match folds.as_ref() {
|
||||||
|
Some(map) => map.nth_visible_from(view_top, coord.row as usize),
|
||||||
|
None => view_top.saturating_add(coord.row as usize),
|
||||||
|
};
|
||||||
|
let display_row = u32::try_from(display_row).ok()?;
|
||||||
|
let target = crate::view::DisplayCoord::new(display_row, coord.col);
|
||||||
|
let registry = core.registry.clone();
|
||||||
|
let reg = registry.borrow();
|
||||||
|
let buf = reg.get(buffer_id).ok()?;
|
||||||
|
core.windows[&win_id]
|
||||||
|
.text_view
|
||||||
|
.display_to_pos(buf, target, core.layout_ctx(win_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move ONE window's point, never the ambient active window's.
|
||||||
|
fn panel_set_cursor(&self, win_id: WindowId, byte: u64) {
|
||||||
|
let mut core = self.core.borrow_mut();
|
||||||
|
if let Some(window) = core.windows.get_mut(&win_id) {
|
||||||
|
window.cursor = byte;
|
||||||
|
window.goal_col = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set or clear ONE window's selection anchor.
|
||||||
|
fn panel_set_selection(&self, win_id: WindowId, anchor: Option<u64>) {
|
||||||
|
let mut core = self.core.borrow_mut();
|
||||||
|
if let Some(window) = core.windows.get_mut(&win_id) {
|
||||||
|
window.selection = anchor.map(|anchor| crate::window::Selection { anchor });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Precompute owned terminal view snapshots before entering paint borrows.
|
/// Precompute owned terminal view snapshots before entering paint borrows.
|
||||||
|
|
@ -3657,6 +4235,12 @@ impl EditorState {
|
||||||
/// A second copy of this precedence in the GPU lane is exactly how
|
/// A second copy of this precedence in the GPU lane is exactly how
|
||||||
/// Shift-drag or scrolled-back selection would silently diverge
|
/// Shift-drag or scrolled-back selection would silently diverge
|
||||||
/// between frontends.
|
/// between frontends.
|
||||||
|
/// Returns WHICH WAY it routed the gesture (G5k).
|
||||||
|
///
|
||||||
|
/// The route has to be measured where the branch is taken, not
|
||||||
|
/// predicted from the modes beforehand: the report is gated on five
|
||||||
|
/// conditions and `encode_mouse` can still decline. A panel press
|
||||||
|
/// records the answer and binds its whole gesture to it.
|
||||||
fn apply_terminal_gesture(
|
fn apply_terminal_gesture(
|
||||||
&mut self,
|
&mut self,
|
||||||
key: TerminalViewKey,
|
key: TerminalViewKey,
|
||||||
|
|
@ -3665,12 +4249,12 @@ impl EditorState {
|
||||||
kind: TerminalMouseKind,
|
kind: TerminalMouseKind,
|
||||||
modifiers: TerminalModifiers,
|
modifiers: TerminalModifiers,
|
||||||
global: (u32, u32),
|
global: (u32, u32),
|
||||||
) {
|
) -> TerminalGestureRoute {
|
||||||
let shift = modifiers.contains(TerminalModifiers::SHIFT);
|
let shift = modifiers.contains(TerminalModifiers::SHIFT);
|
||||||
let (at_bottom, modes, screen_size) = {
|
let (at_bottom, modes, screen_size) = {
|
||||||
let mut manager = self.terminal_manager.borrow_mut();
|
let mut manager = self.terminal_manager.borrow_mut();
|
||||||
let Some(status) = manager.view_status_for_size(key, viewport_size) else {
|
let Some(status) = manager.view_status_for_size(key, viewport_size) else {
|
||||||
return;
|
return TerminalGestureRoute::None;
|
||||||
};
|
};
|
||||||
let modes = manager.modes_for_view(key).unwrap_or_default();
|
let modes = manager.modes_for_view(key).unwrap_or_default();
|
||||||
let screen_size = manager.screen_size_for_view(key).unwrap_or(viewport_size);
|
let screen_size = manager.screen_size_for_view(key).unwrap_or(viewport_size);
|
||||||
|
|
@ -3700,12 +4284,13 @@ impl EditorState {
|
||||||
self.claim_terminal_controller(key);
|
self.claim_terminal_controller(key);
|
||||||
}
|
}
|
||||||
self.send_terminal_bytes(key.buffer_id, &bytes);
|
self.send_terminal_bytes(key.buffer_id, &bytes);
|
||||||
return;
|
return TerminalGestureRoute::Child(modes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims_control {
|
if claims_control {
|
||||||
self.claim_terminal_controller(key);
|
self.claim_terminal_controller(key);
|
||||||
}
|
}
|
||||||
|
let mut began_local_gesture = false;
|
||||||
let mut manager = self.terminal_manager.borrow_mut();
|
let mut manager = self.terminal_manager.borrow_mut();
|
||||||
match kind {
|
match kind {
|
||||||
TerminalMouseKind::ScrollUp => {
|
TerminalMouseKind::ScrollUp => {
|
||||||
|
|
@ -3715,7 +4300,12 @@ impl EditorState {
|
||||||
let _ = manager.scroll_view(key, viewport_size, -SCROLL_LINES);
|
let _ = manager.scroll_view(key, viewport_size, -SCROLL_LINES);
|
||||||
}
|
}
|
||||||
TerminalMouseKind::Down(TerminalMouseButton::Left) => {
|
TerminalMouseKind::Down(TerminalMouseButton::Left) => {
|
||||||
let _ = manager.begin_selection(key, viewport_size, coord);
|
// The ONE local kind that begins a gesture, so its
|
||||||
|
// answer decides whether there is anything to arm. A
|
||||||
|
// press against a view that cannot start a drag began
|
||||||
|
// nothing, and arming over it records a gesture whose
|
||||||
|
// completion has nothing to finish.
|
||||||
|
began_local_gesture = manager.begin_selection(key, viewport_size, coord);
|
||||||
}
|
}
|
||||||
TerminalMouseKind::Drag(TerminalMouseButton::Left) => {
|
TerminalMouseKind::Drag(TerminalMouseButton::Left) => {
|
||||||
let _ = manager.update_selection(key, viewport_size, coord);
|
let _ = manager.update_selection(key, viewport_size, coord);
|
||||||
|
|
@ -3731,6 +4321,19 @@ impl EditorState {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
// The local branch: scrollback, local selection or the menu. The
|
||||||
|
// child heard nothing.
|
||||||
|
//
|
||||||
|
// A left press reports `Local` only when it actually began a
|
||||||
|
// drag; otherwise it began nothing and the caller must not arm.
|
||||||
|
// Every other kind is not an arming event, so `Local` is the
|
||||||
|
// honest answer for it either way.
|
||||||
|
if matches!(kind, TerminalMouseKind::Down(TerminalMouseButton::Left))
|
||||||
|
&& !began_local_gesture
|
||||||
|
{
|
||||||
|
return TerminalGestureRoute::None;
|
||||||
|
}
|
||||||
|
TerminalGestureRoute::Local
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_double_click(
|
fn is_double_click(
|
||||||
|
|
|
||||||
|
|
@ -174,11 +174,24 @@ pub struct AcceptedPanelGesture {
|
||||||
pub coord: pmacs_protocol::CellCoord,
|
pub coord: pmacs_protocol::CellCoord,
|
||||||
/// The panel buffer the gesture belongs to.
|
/// The panel buffer the gesture belongs to.
|
||||||
pub buffer_id: BufferId,
|
pub buffer_id: BufferId,
|
||||||
/// Whether the press actually REACHED the child, for a reporting
|
/// The domain the accepted press RESOLVED INTO, and which every
|
||||||
/// terminal. A release is owed only if a press was delivered;
|
/// tail and the completion must follow (G5k).
|
||||||
/// synthesising one for a press the child never saw is the same
|
///
|
||||||
/// defect in the other direction.
|
/// This replaces a bare `reached_child` flag. The flag said whether
|
||||||
pub reached_child: bool,
|
/// a release was owed to a child but not how to frame it, nor which
|
||||||
|
/// window to address, so a tail had to re-derive both from state
|
||||||
|
/// that moves mid-gesture — Shift, the scroll position, the child's
|
||||||
|
/// modes, and the panel's own identity. Recording the resolution
|
||||||
|
/// is what makes the tail independent of all four.
|
||||||
|
pub domain: crate::editor::PanelGestureDomain,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcceptedPanelGesture {
|
||||||
|
/// Whether the press reached the child, so a release is OWED to it.
|
||||||
|
#[must_use]
|
||||||
|
pub fn reached_child(&self) -> bool {
|
||||||
|
self.domain.reached_child()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Owns one `semantic_render` session's projection state: the last
|
/// Owns one `semantic_render` session's projection state: the last
|
||||||
|
|
@ -451,6 +464,21 @@ pub struct SemanticRenderState {
|
||||||
/// match the press it ends, and a stale `Up` with no accepted
|
/// match the press it ends, and a stale `Up` with no accepted
|
||||||
/// `Down` must be inert rather than synthesising one.
|
/// `Down` must be inert rather than synthesising one.
|
||||||
accepted_gesture: Option<AcceptedPanelGesture>,
|
accepted_gesture: Option<AcceptedPanelGesture>,
|
||||||
|
/// §5b/parent 48 — a release this frontend is OWED, parked until
|
||||||
|
/// somewhere that can deliver it.
|
||||||
|
///
|
||||||
|
/// Two of the three cancellation sites are inside frame production
|
||||||
|
/// — the mapping-generation advance and `publish_absent_panel` —
|
||||||
|
/// where no target effect can run. `cancel_accepted_gesture`
|
||||||
|
/// returned the record into those contexts and they dropped it, so
|
||||||
|
/// the gesture ended with the child still holding its button.
|
||||||
|
///
|
||||||
|
/// **A SLOT, not a queue.** The latch holds at most one gesture per
|
||||||
|
/// frontend, so at most one release can be owed, and the bound is
|
||||||
|
/// structural rather than a cap someone had to choose. The drain
|
||||||
|
/// runs ahead of the next panel-pointer effect, so a second
|
||||||
|
/// cancellation cannot arrive while one is still parked.
|
||||||
|
pending_release: Option<AcceptedPanelGesture>,
|
||||||
/// §5b — how many armed gestures an authority loss has ended this
|
/// §5b — how many armed gestures an authority loss has ended this
|
||||||
/// session.
|
/// session.
|
||||||
///
|
///
|
||||||
|
|
@ -504,6 +532,15 @@ struct PanelPresentation {
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
buffer_id: BufferId,
|
buffer_id: BufferId,
|
||||||
panel_epoch: u64,
|
panel_epoch: u64,
|
||||||
|
/// The geometry epoch this presentation was shipped under.
|
||||||
|
///
|
||||||
|
/// Retained for parent 48's authority-loss matrix: a geometry
|
||||||
|
/// change at an UNCHANGED `CellSize` moves nothing else the
|
||||||
|
/// producer holds — not the panel epoch, not the identity, and on a
|
||||||
|
/// legacy peer not a mapping key either — so without this the
|
||||||
|
/// transition is invisible and a live gesture survives a grid it no
|
||||||
|
/// longer belongs to.
|
||||||
|
geometry_epoch: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One [`SemanticRenderState::diag_line_cache`] entry: the line-start
|
/// One [`SemanticRenderState::diag_line_cache`] entry: the line-start
|
||||||
|
|
@ -673,6 +710,7 @@ impl SemanticRenderState {
|
||||||
panel_mapping: None,
|
panel_mapping: None,
|
||||||
panel_mapping_exhausted: false,
|
panel_mapping_exhausted: false,
|
||||||
accepted_gesture: None,
|
accepted_gesture: None,
|
||||||
|
pending_release: None,
|
||||||
panel_gesture_cancellations: 0,
|
panel_gesture_cancellations: 0,
|
||||||
panel_epoch_used: 0,
|
panel_epoch_used: 0,
|
||||||
panel_presentation: None,
|
panel_presentation: None,
|
||||||
|
|
@ -728,12 +766,30 @@ impl SemanticRenderState {
|
||||||
/// per §5b's split table, because the release it denies does not
|
/// per §5b's split table, because the release it denies does not
|
||||||
/// exist on this branch.
|
/// exist on this branch.
|
||||||
pub fn arm_accepted_gesture(&mut self, gesture: AcceptedPanelGesture) {
|
pub fn arm_accepted_gesture(&mut self, gesture: AcceptedPanelGesture) {
|
||||||
// A second accepted press while one is already armed means the
|
// THE INVARIANT IS ASSERTED WHERE IT IS RELIED ON. A caller must
|
||||||
// first gesture's release never arrived — a dropped `Up`, or an
|
// have ended AND paid any live gesture before arming a
|
||||||
// outbox that closed under a stall. END it rather than
|
// replacement: a second accepted press while one is armed means
|
||||||
// overwrite it: overwriting discards the record silently, and
|
// the first gesture's release never arrived, and its release has
|
||||||
// once replay attaches effects to that record the child is left
|
// to reach the target before this press does. Cancelling here
|
||||||
// holding a button down with nothing left to release it.
|
// instead is too late by exactly one effect --- the replacement
|
||||||
|
// has already landed.
|
||||||
|
//
|
||||||
|
// Checked at the point of arming rather than inside
|
||||||
|
// cancellation, because arming is what the ordering protects.
|
||||||
|
debug_assert!(
|
||||||
|
self.accepted_gesture.is_none(),
|
||||||
|
"arming over a LIVE gesture: the caller must cancel and \
|
||||||
|
drain it first, or the replacement press overtakes the old \
|
||||||
|
gesture's release"
|
||||||
|
);
|
||||||
|
debug_assert!(
|
||||||
|
self.pending_release.is_none(),
|
||||||
|
"arming over an OWED release: the drain must run before the \
|
||||||
|
effect that arms"
|
||||||
|
);
|
||||||
|
// Defensive in release builds: ending it parks the record for a
|
||||||
|
// later drain, which is late but not lost. Overwriting would
|
||||||
|
// discard it outright and leave the child holding a button.
|
||||||
self.cancel_accepted_gesture();
|
self.cancel_accepted_gesture();
|
||||||
self.accepted_gesture = Some(gesture);
|
self.accepted_gesture = Some(gesture);
|
||||||
}
|
}
|
||||||
|
|
@ -763,12 +819,61 @@ impl SemanticRenderState {
|
||||||
/// without inventing a second cancellation.
|
/// without inventing a second cancellation.
|
||||||
pub fn cancel_accepted_gesture(&mut self) -> Option<AcceptedPanelGesture> {
|
pub fn cancel_accepted_gesture(&mut self) -> Option<AcceptedPanelGesture> {
|
||||||
let cancelled = self.accepted_gesture.take();
|
let cancelled = self.accepted_gesture.take();
|
||||||
if cancelled.is_some() {
|
if let Some(record) = cancelled {
|
||||||
self.panel_gesture_cancellations = self.panel_gesture_cancellations.saturating_add(1);
|
self.panel_gesture_cancellations = self.panel_gesture_cancellations.saturating_add(1);
|
||||||
|
// PARKED, not just returned. Returning was the whole bug:
|
||||||
|
// the callers inside frame production cannot deliver a
|
||||||
|
// release, so the record went out of scope and the gesture
|
||||||
|
// ended with nothing terminated.
|
||||||
|
//
|
||||||
|
// Overwriting a still-parked release would lose one, so it
|
||||||
|
// is an invariant violation rather than a silent drop. It is
|
||||||
|
// a BACKSTOP: the ordering — drain before the next effect —
|
||||||
|
// is what actually prevents it.
|
||||||
|
debug_assert!(
|
||||||
|
self.pending_release.is_none(),
|
||||||
|
"a release was still owed when another gesture was \
|
||||||
|
cancelled; the drain must run before any subsequent \
|
||||||
|
panel-pointer effect"
|
||||||
|
);
|
||||||
|
self.pending_release = Some(record);
|
||||||
}
|
}
|
||||||
cancelled
|
cancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take the release this frontend is owed, if any.
|
||||||
|
///
|
||||||
|
/// The RETURN of `cancel_accepted_gesture` is for inspection; THIS
|
||||||
|
/// is the delivery path.
|
||||||
|
pub fn take_pending_release(&mut self) -> Option<AcceptedPanelGesture> {
|
||||||
|
self.pending_release.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a release is still owed, for assertions.
|
||||||
|
#[must_use]
|
||||||
|
pub fn has_pending_release(&self) -> bool {
|
||||||
|
self.pending_release.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The live gesture record, if any — parent 48 Q#BP-R4's
|
||||||
|
/// "live record" test, and the source of the recorded completion.
|
||||||
|
#[must_use]
|
||||||
|
pub fn accepted_gesture(&self) -> Option<&AcceptedPanelGesture> {
|
||||||
|
self.accepted_gesture.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Q#BP-R4: an accepted `Drag` continues the gesture and moves its
|
||||||
|
/// LAST VALID CONTENT CELL, which is where a release that later
|
||||||
|
/// lands on chrome gets delivered (R-c2).
|
||||||
|
///
|
||||||
|
/// Inert with nothing armed. A drag with no accepted press is a
|
||||||
|
/// stale tail and must not create a record by writing to one.
|
||||||
|
pub fn note_gesture_content_cell(&mut self, coord: pmacs_protocol::CellCoord) {
|
||||||
|
if let Some(gesture) = self.accepted_gesture.as_mut() {
|
||||||
|
gesture.coord = coord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a gesture is currently accepted, for assertions.
|
/// Whether a gesture is currently accepted, for assertions.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn has_accepted_gesture(&self) -> bool {
|
pub fn has_accepted_gesture(&self) -> bool {
|
||||||
|
|
@ -1624,6 +1729,31 @@ impl SemanticRenderState {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let identity = (projection.window_id, projection.buffer_id);
|
let identity = (projection.window_id, projection.buffer_id);
|
||||||
|
|
||||||
|
// Parent 48 G5b — AUTHORITY LOSS. A live gesture belongs to the
|
||||||
|
// presentation it was pressed on. Three of the matrix's five
|
||||||
|
// transitions are visible right here, and each is a separate
|
||||||
|
// cause with one shared consequence:
|
||||||
|
//
|
||||||
|
// * the side WINDOW was replaced (panel-epoch change),
|
||||||
|
// * its BUFFER was replaced,
|
||||||
|
// * the GEOMETRY epoch moved, including at an unchanged size.
|
||||||
|
//
|
||||||
|
// §5b left these armed deliberately — inert while nothing
|
||||||
|
// consumed the latch — and they became defects the moment this
|
||||||
|
// lane gave cancellation an effect. Taking the latch parks the
|
||||||
|
// release; the drain pays it.
|
||||||
|
//
|
||||||
|
// G5m: coincident causes take the latch ONCE. `cancel_accepted_gesture`
|
||||||
|
// is idempotent on an empty latch, so a transition that trips
|
||||||
|
// two of these conditions still emits one release.
|
||||||
|
if let Some(presentation) = self.panel_presentation
|
||||||
|
&& ((presentation.window_id, presentation.buffer_id) != identity
|
||||||
|
|| presentation.geometry_epoch != geometry.geometry_epoch)
|
||||||
|
{
|
||||||
|
self.cancel_accepted_gesture();
|
||||||
|
}
|
||||||
|
|
||||||
let panel_epoch = match self.panel_presentation {
|
let panel_epoch = match self.panel_presentation {
|
||||||
Some(presentation) if (presentation.window_id, presentation.buffer_id) == identity => {
|
Some(presentation) if (presentation.window_id, presentation.buffer_id) == identity => {
|
||||||
Some(presentation.panel_epoch)
|
Some(presentation.panel_epoch)
|
||||||
|
|
@ -1707,6 +1837,7 @@ impl SemanticRenderState {
|
||||||
window_id: projection.window_id,
|
window_id: projection.window_id,
|
||||||
buffer_id: projection.buffer_id,
|
buffer_id: projection.buffer_id,
|
||||||
panel_epoch,
|
panel_epoch,
|
||||||
|
geometry_epoch: geometry.geometry_epoch,
|
||||||
});
|
});
|
||||||
self.last_panel_payload = Some(payload.clone());
|
self.last_panel_payload = Some(payload.clone());
|
||||||
out.push(InstanceMessage::PanelFrame(payload));
|
out.push(InstanceMessage::PanelFrame(payload));
|
||||||
|
|
@ -1745,14 +1876,12 @@ impl SemanticRenderState {
|
||||||
// wire work but must still leave no live gesture behind — the
|
// wire work but must still leave no live gesture behind — the
|
||||||
// clears below are idempotent for the same reason.
|
// clears below are idempotent for the same reason.
|
||||||
//
|
//
|
||||||
// Only the producer half lands here. The framing's G5b matrix —
|
// `Absent` is one of G5b's five transitions. THE OTHER FOUR ARE
|
||||||
// the panel-epoch, buffer-replacement, same-size geometry and
|
// NOW WIRED TOO, on this branch: window replacement, buffer
|
||||||
// detach transitions, each with its v24 and v25 legs and its
|
// replacement and a geometry-epoch change are handled where the
|
||||||
// document and terminal effects — is `panel-pointer-replay`'s per
|
// declaration is built above, and detach in the dispatcher's
|
||||||
// §5b's split table. Those transitions leave the latch armed on
|
// teardown. This comment used to say they were left armed — true
|
||||||
// this branch, which is inert here (nothing consumes it) and
|
// of §5b, false here since the authority-loss matrix landed.
|
||||||
// becomes a defect only once replay gives it effects, in the
|
|
||||||
// branch that owns the row.
|
|
||||||
self.cancel_accepted_gesture();
|
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
|
||||||
|
|
|
||||||
|
|
@ -619,6 +619,22 @@ impl TerminalScreen {
|
||||||
pub fn modes(&self) -> TerminalModes {
|
pub fn modes(&self) -> TerminalModes {
|
||||||
self.modes
|
self.modes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn SGR mouse reporting on or off, for parent 48 G5k.
|
||||||
|
///
|
||||||
|
/// The domain matrix needs a child that reports and then STOPS
|
||||||
|
/// reporting mid-gesture. Driving that through the ANSI parser
|
||||||
|
/// would make the row depend on escape-sequence handling it is not
|
||||||
|
/// testing; this sets the two modes the adapter actually reads.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn set_mouse_reporting_for_test(&mut self, enabled: bool) {
|
||||||
|
self.modes.mouse_sgr = enabled;
|
||||||
|
self.modes.mouse_tracking = if enabled {
|
||||||
|
MouseTrackingMode::Button
|
||||||
|
} else {
|
||||||
|
MouseTrackingMode::Off
|
||||||
|
};
|
||||||
|
}
|
||||||
/// Return whether the published active screen is alternate.
|
/// Return whether the published active screen is alternate.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn alternate_active(&self) -> bool {
|
pub fn alternate_active(&self) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -247,10 +247,21 @@ pub(super) struct EscapeCache {
|
||||||
pub(super) reported_invalid: Option<String>,
|
pub(super) reported_invalid: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What each child was sent, in order, while the G5k tap is armed.
|
||||||
|
type ChildSendLog = Vec<(BufferId, Vec<u8>)>;
|
||||||
|
|
||||||
/// Owns the one-buffer/one-process/one-screen terminal registry.
|
/// Owns the one-buffer/one-process/one-screen terminal registry.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct TerminalManager {
|
pub struct TerminalManager {
|
||||||
pub(super) sessions: HashMap<BufferId, TerminalSession>,
|
pub(super) sessions: HashMap<BufferId, TerminalSession>,
|
||||||
|
/// An OPT-IN tap on child input, for parent 48 G5k's witnesses.
|
||||||
|
///
|
||||||
|
/// The gesture-domain rows have to read what the child actually
|
||||||
|
/// received --- a release delivered in the recorded encoding, and
|
||||||
|
/// exactly one of it --- and no other seam exposes that. Off by
|
||||||
|
/// default, so production pays one `is_some` check per send and
|
||||||
|
/// never accumulates.
|
||||||
|
send_tap: RefCell<Option<ChildSendLog>>,
|
||||||
/// Total escape-key parses performed (Q#TC4c observability).
|
/// Total escape-key parses performed (Q#TC4c observability).
|
||||||
escape_parses: u64,
|
escape_parses: u64,
|
||||||
process_to_buffer: HashMap<ProcessId, BufferId>,
|
process_to_buffer: HashMap<ProcessId, BufferId>,
|
||||||
|
|
@ -564,11 +575,35 @@ impl TerminalManager {
|
||||||
.sessions
|
.sessions
|
||||||
.get(&buffer_id)
|
.get(&buffer_id)
|
||||||
.ok_or(TerminalError::NotTerminal(buffer_id))?;
|
.ok_or(TerminalError::NotTerminal(buffer_id))?;
|
||||||
|
if let Some(tap) = self.send_tap.borrow_mut().as_mut() {
|
||||||
|
tap.push((buffer_id, bytes.to_vec()));
|
||||||
|
}
|
||||||
supervisor
|
supervisor
|
||||||
.write_stdin(session.process_id, bytes)
|
.write_stdin(session.process_id, bytes)
|
||||||
.map_err(TerminalError::Process)
|
.map_err(TerminalError::Process)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Begin recording child input for G5k's witnesses.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn start_send_tap_for_test(&self) {
|
||||||
|
*self.send_tap.borrow_mut() = Some(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take everything sent to children since the tap was started.
|
||||||
|
///
|
||||||
|
/// Returns the sends in ORDER, because "one release, not two" and
|
||||||
|
/// "the old gesture's release before the new gesture's press" are
|
||||||
|
/// both ordering claims that a set cannot express.
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn take_send_tap_for_test(&self) -> ChildSendLog {
|
||||||
|
self.send_tap
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.map(std::mem::take)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve this terminal's effective escape chord, parsing at most
|
/// Resolve this terminal's effective escape chord, parsing at most
|
||||||
/// once per `(terminal, config epoch)` (Q#TC4c).
|
/// once per `(terminal, config epoch)` (Q#TC4c).
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -501,6 +501,22 @@ impl TerminalManager {
|
||||||
moved || was_dragging
|
moved || was_dragging
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a view is mid-drag, for parent 48 Q#BP-R4's completion
|
||||||
|
/// witnesses.
|
||||||
|
///
|
||||||
|
/// A local terminal gesture is "finished" exactly when
|
||||||
|
/// `finish_selection` takes `drag`, so this is the observable that
|
||||||
|
/// separates a delivered completion from a latch that merely
|
||||||
|
/// emptied — which is the distinction the framing requires those
|
||||||
|
/// rows to assert.
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn view_is_dragging_for_test(&self, key: TerminalViewKey) -> bool {
|
||||||
|
self.views
|
||||||
|
.get(&key)
|
||||||
|
.is_some_and(|state| state.drag.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear one view's terminal selection without changing its scroll anchor.
|
/// Clear one view's terminal selection without changing its scroll anchor.
|
||||||
pub fn clear_selection(&mut self, key: TerminalViewKey) -> bool {
|
pub fn clear_selection(&mut self, key: TerminalViewKey) -> bool {
|
||||||
let Some(state) = self.views.get_mut(&key) else {
|
let Some(state) = self.views.get_mut(&key) else {
|
||||||
|
|
@ -509,6 +525,14 @@ impl TerminalManager {
|
||||||
state.selection.take().is_some() || state.drag.take().is_some()
|
state.selection.take().is_some() || state.drag.take().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn SGR mouse reporting on or off for one session (G5k).
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn set_mouse_reporting_for_test(&mut self, buffer_id: BufferId, enabled: bool) {
|
||||||
|
if let Some(session) = self.sessions.get_mut(&buffer_id) {
|
||||||
|
session.screen.set_mouse_reporting_for_test(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Current child input modes for one session.
|
/// Current child input modes for one session.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn modes_for_view(&self, key: TerminalViewKey) -> Option<TerminalModes> {
|
pub fn modes_for_view(&self, key: TerminalViewKey) -> Option<TerminalModes> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue