docs(framing): bottom-panel revision 8 --- target x origin, and a negative row standing alone
Answers review of revision 7. Still framing only; no implementation.
**R-c NEEDED TARGET x GESTURE-ORIGIN, NOT KIND ALONE.** The per-kind
rule I took from the TUI is DOCUMENT-ONLY: for a terminal window the
TUI rejects every kind on the mode line before any per-kind match
(`src/editor.rs:3273`) and hands `dispatch_terminal_mouse` a
content-sized viewport (`:3272`). Terminals never see a chrome
coordinate.
And passing one is not merely untidy, it is wrong twice:
`apply_terminal_gesture`'s reporting branch is bounds-checked on
`coord.row < screen_size.rows` (`:3560`), and a chrome row equals
`screen_size.rows`, so the check FAILS and the gesture falls into the
LOCAL branch. A reporting child therefore receives a `Down` and then no
`Up`, and a chrome wheel silently becomes local scrollback instead of
behaving like a content wheel. The table is now split by target, and a
content-originated gesture terminates at its LAST VALID CONTENT
COORDINATE.
**Q#BP-R2 is ruled: a chrome wheel over a terminal panel CLAMPS.** This
diverges from the TUI, which drops it, and is flagged as the one
product call inside R-c. The panel's mode line is the band's own
chrome, one row, and a document panel already scrolls on a chrome
wheel; dropping only for terminals would make the same pixel behave
differently depending on which buffer happens to be shown. Overrule if
TUI parity is preferred --- nothing else in R-c depends on it.
**R-c2 --- THE PRODUCER NEVER REMEMBERS THE `Down` CELL.**
`set_panel_pointer_held` clears `last_pointer_cell`
(`pmacs-gpu/src/main.rs:7250`) and only motion refills it, while
`panel_release_cell` falls back to exactly that field. Today the
fallback is rarely reached because chrome IS a `PanelCell`; the moment
R-c stops that, a `Down` in content followed immediately by a release
over chrome --- no intervening motion --- has nothing to fall back to.
Retain the `Down` cell at arm time. For a reporting terminal the row
asserts the exact child release BYTES, because a latch that clears
while the child never hears the release is the failure being guarded.
**A1 WAS A NEGATIVE ROW STANDING ALONE.** It asserts bytes that must
not appear, so disabling child reporting entirely satisfies it --- as
does never reaching `apply_terminal_gesture`. The whole edge matrix
could have stayed green against a replay that never reports anything.
A3-A5 add the positive half on the SAME terminal: exact SGR sequences
for `Down`/`Drag`/`Up`, exact wheel bytes when reporting, and the
non-reporting wheel's local scrollback effect.
**Four witness seams tightened.** B2 was checking the wrong thing:
`active_region()` returns `None` "if no region is set OR IT IS EMPTY"
(`src/editor_core.rs:4684`), so it answers None both before and after
the `Up` and cannot see the residue --- which is an active-but-EMPTY
selection whose stale anchor would capture the next shift-motion. It
reads the owning window's raw `selection` now. B4 pins the exact
selected word rather than that click state "reads a multi-click". B6
runs its visit sentinel through single AND double click, since the
ruling forbids both. D4 uses a CHANGED frame with unchanged epochs ---
the focus repaint the `Down` itself causes --- because a byte-identical
duplicate returns early ("A duplicate does no work", `:6918`) and would
never reach the reset code it is meant to constrain.
Gates: all nine green under `env -u TMPDIR`, log 20260814T122442Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
d9a23f30eb
commit
4833a73dfe
|
|
@ -281,12 +281,16 @@ from #171 and #215 — the correction the 1b lane missed, honoured here.
|
|||
**`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 7 (§5a) AWAITING APPROVAL;
|
||||
- **No PR yet. Checkpoint: framing revision 8 (§5a) AWAITING APPROVAL;
|
||||
NO IMPLEMENTATION WRITTEN.** Commit one was the ground-truth
|
||||
re-measurement; 6 added the four replay edges; **7 answers review of
|
||||
6** — R-c is per-KIND with a producer rule, R-d covers geometry
|
||||
identity and both latch fields, R-a gains the document Shift
|
||||
contrast, R-b's rows now pin results.
|
||||
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.
|
||||
- **Why this lane exists.** `PanelPointer` **replays nothing**:
|
||||
`dispatch_semantic_panel_pointer` (`src/editor.rs:2674`) validates,
|
||||
focuses, returns. A panel wheel is dead on both axes and so is every
|
||||
|
|
|
|||
|
|
@ -7,8 +7,39 @@ protocol v20, 2026-07-24. Amended by the pre-implementation dependency
|
|||
verification in §0.6: the folding dependency is cleared, and one geometry
|
||||
caller-census error is corrected.**
|
||||
|
||||
**Revision 7 — 2026-08-14, AWAITING APPROVAL.** Answers review of 6.
|
||||
Five contract gaps, three of them corrections to 6's own rules:
|
||||
**Revision 8 — 2026-08-14, AWAITING APPROVAL.** Answers review of 7:
|
||||
|
||||
- **R-c is target × gesture-ORIGIN, not kind alone.** The TUI's
|
||||
per-kind rule is **document-only** — for terminals it rejects every
|
||||
kind on the mode line (`src/editor.rs:3273`) and passes a
|
||||
content-sized viewport. Raw chrome coordinates are actively unsafe
|
||||
there: `apply_terminal_gesture`'s reporting branch is bounds-checked
|
||||
(`:3560`), so a chrome row **falls through to the LOCAL branch** — a
|
||||
reporting child gets `Down` and no `Up`, and a chrome wheel becomes
|
||||
local scrollback. **A content-originated gesture terminates at its
|
||||
last valid CONTENT coordinate.** New: **Q#BP-R2**, ruling that a
|
||||
chrome wheel over a terminal panel **clamps** rather than dropping —
|
||||
a deliberate divergence from the TUI, flagged for overrule.
|
||||
- **R-c2: the producer never remembers the `Down` cell.** Arming clears
|
||||
`last_pointer_cell` (`pmacs-gpu/src/main.rs:7250`) and only motion
|
||||
refills it, so once chrome stops being a `PanelCell`, a `Down` +
|
||||
immediate release has **no fallback coordinate**. Retain it at arm
|
||||
time; for a reporting terminal the row asserts the **exact child
|
||||
release bytes**.
|
||||
- **A1 was a negative row standing alone** — disabling reporting
|
||||
entirely satisfies it. **A3–A5** add positive controls on the same
|
||||
terminal: exact SGR bytes for `Down`/`Drag`/`Up` and for the wheel,
|
||||
plus the non-reporting wheel's local scrollback effect.
|
||||
- **Four witness seams tightened.** B2 reads the **raw `selection`**,
|
||||
because `active_region()` answers `None` on both sides of the `Up`
|
||||
(`src/editor_core.rs:4684`); B4 pins the **exact selected word**; B6
|
||||
runs its sentinel through **single and double** click; D4 uses a
|
||||
**changed** frame with unchanged epochs — the `Down`'s own focus
|
||||
repaint — since a byte-identical duplicate is suppressed before the
|
||||
reset could run (`pmacs-gpu/src/main.rs:6918`).
|
||||
|
||||
**Previously, revision 7 — SUPERSEDED.** Five contract gaps, three of
|
||||
them corrections to 6's own rules:
|
||||
|
||||
- **R-c's "the last row is inert" was wrong.** The TUI's rule is **per
|
||||
KIND**: `inner_rows` guards `Down(Left)`/`Drag(Left)`/`Down(Right)`
|
||||
|
|
@ -1948,6 +1979,23 @@ A2's mutation is deliberately separate from A1's: a single "drop
|
|||
`mods`" at the boundary bites both, which proves the boundary matters
|
||||
but not that **each consumer** is wired.
|
||||
|
||||
**A1 is a NEGATIVE row and cannot stand alone.** It asserts bytes that
|
||||
must *not* appear, and **disabling child reporting entirely satisfies
|
||||
it** — as does never reaching `apply_terminal_gesture` at all. The
|
||||
whole edge matrix could stay green against a replay that never reports.
|
||||
Positive controls, on the **same terminal** as A1 so the pair is a
|
||||
genuine discriminator:
|
||||
|
||||
| # | row | mutation |
|
||||
|---|---|---|
|
||||
| A3 | unmodified content `Down`/`Drag`/`Up`, reporting enabled → the child receives the **exact SGR byte sequences** for each | bypass `apply_terminal_gesture` → no bytes, A3 fails while A1 still passes |
|
||||
| A4 | unmodified content **wheel**, reporting enabled → the child receives the **exact SGR wheel bytes** | as A3 |
|
||||
| A5 | same wheel, reporting **disabled** → **no bytes**, and the view's **local scrollback moves** | route the wheel around the shared path → the scrollback does not move |
|
||||
|
||||
A3–A5 pin the *positive* half: that replay reaches the shared path and
|
||||
the child hears what it should. A1 then pins the one case where it must
|
||||
not.
|
||||
|
||||
#### R-b. Activation does not make the document path target-safe
|
||||
|
||||
`activates` is true for a document panel's `Down(_)` only, so **`Drag`
|
||||
|
|
@ -1975,11 +2023,25 @@ that drops the tail entirely and changes nothing anywhere.
|
|||
| # | row | what it pins |
|
||||
|---|---|---|
|
||||
| B1 | panel A `Down` → **frontend B input** → panel A `Drag` | A's **anchor is the Down cell's byte and A's cursor is the Drag cell's byte**, exactly; B's window and the document mirror are unchanged |
|
||||
| B2 | panel A `Down` → `Up` at the **same** cell | the empty selection **collapses** — an Up that does nothing leaves a stale one-byte region |
|
||||
| B2 | panel A `Down` → `Up` at the **same** cell | the window's **raw `selection` is `None`** afterwards — see below |
|
||||
| B3 | orphan `Drag`/`Up` on a **passive** panel, no preceding `Down` | the document mirror is **byte-identical**: cursor, selection, `view_top` |
|
||||
| B4 | **repeated left `Down`s** at one cell | the existing daemon click state reads a **multi-click**, per Q#BP16 — so replay must not swallow or coalesce them |
|
||||
| B4 | **repeated left `Down`s** at one cell | the **exact word** at that cell is selected after the second `Down` — the multi-click actually resolved, per Q#BP16 |
|
||||
| B5 | `Down(Right)` on a panel cell | the **context menu** opens, per Q#BP16 — a right press is not a selection gesture |
|
||||
| B6 | click a **listview** row | the row is **selected** and `on_visit` **does not run** — a sentinel proving Q#BP-R1's ruling holds through replay |
|
||||
| B6 | **single** click, then **repeated/double** click, on a listview row | the row is **selected** and the `on_visit` sentinel **never fires**, in *both* — Q#BP-R1 forbids double-click-to-visit too |
|
||||
|
||||
**B2 must read the raw selection, not `active_region()`.** That helper
|
||||
returns `None` *"if no region is set **or it is empty**"*
|
||||
(`src/editor_core.rs:4684`–`:4688`), so it answers `None` **both before
|
||||
and after** the `Up` and cannot see the residue at all. The residue
|
||||
being guarded against is an **active-but-empty selection** — a
|
||||
`Some(Selection { anchor })` whose anchor equals the cursor — which
|
||||
would make the next shift-motion extend from a stale anchor. Inspect
|
||||
the **owning window's** `selection` field directly.
|
||||
|
||||
**B6 runs both click shapes** because the ruling forbids
|
||||
double-click-to-visit as well as single. A sentinel that only tries a
|
||||
single click leaves the more tempting implementation — visit on the
|
||||
second click, like a file manager — completely uncovered.
|
||||
|
||||
B4 and B5 are not new contracts: Q#BP16 already states that repeated
|
||||
left `Down`s are what the click state reads as a multi-click and that
|
||||
|
|
@ -2027,28 +2089,94 @@ Dragging from there into content then emits a `Drag` with no accepted
|
|||
receiver-only rule cannot fix this**; the producer must not arm on a
|
||||
mode-line press.
|
||||
|
||||
| kind on the mode-line row | producer | receiver |
|
||||
|---|---|---|
|
||||
| `Down(Left)`, `Down(Right)` | **do not arm, do not send** | reserved — drop |
|
||||
| `Drag(Left)` | not sent (never armed) | reserved — drop |
|
||||
| `Up(Left)` | **send** — it terminates a gesture begun in content | **process**, terminating the gesture |
|
||||
| wheel | **send** | **process** — scrolls the panel, per the TUI |
|
||||
**Kind alone is still not enough. The rule is target × gesture
|
||||
ORIGIN**, and revision 7's table had neither axis complete.
|
||||
|
||||
**The TUI's per-kind rule is DOCUMENT-ONLY.** For a terminal window it
|
||||
rejects **every** kind on the mode line before any per-kind match:
|
||||
`if local_row >= inner_rows || … { self.mouse_click = None; return; }`
|
||||
(`src/editor.rs:3273`), and it passes the content-sized viewport
|
||||
`CellSize::new(inner_rows, …)` (`:3272`) to `dispatch_terminal_mouse`.
|
||||
Terminals never see a chrome coordinate at all.
|
||||
|
||||
**And raw chrome coordinates are actively unsafe for terminals**,
|
||||
because `apply_terminal_gesture`'s reporting branch is bounds-checked:
|
||||
`coord.row < screen_size.rows && coord.col < screen_size.cols`
|
||||
(`src/editor.rs:3560`–`:3561`). A chrome row equals `screen_size.rows`,
|
||||
so the check **fails and the gesture falls through to the LOCAL
|
||||
branch**. Two concrete wrongnesses follow: a reporting child gets a
|
||||
`Down` and then **no `Up`** (its release became a local
|
||||
`finish_selection`), and a chrome wheel becomes **local scrollback**
|
||||
instead of behaving as a content wheel does.
|
||||
|
||||
| target | kind | producer | receiver |
|
||||
|---|---|---|---|
|
||||
| **document** | `Down(Left)`, `Down(Right)` | do not arm, do not send | reserved — drop |
|
||||
| **document** | `Drag(Left)`, chrome-originated | not sent (never armed) | reserved — drop |
|
||||
| **document** | `Drag(Left)`, **content-originated** | **sent** — the gesture is live | **process at the last valid CONTENT coordinate** |
|
||||
| **document** | `Up(Left)` | **send** | **process** — terminates the gesture |
|
||||
| **document** | wheel | **send** | **process** — scrolls the panel |
|
||||
| **terminal** | any kind, chrome-**originated** | do not arm, do not send | drop, matching the TUI |
|
||||
| **terminal** | `Drag`/`Up`, **content-originated**, now over chrome | **send** | **replay at the last valid CONTENT coordinate** — never the chrome row |
|
||||
| **terminal** | wheel over chrome | **send, clamped to content** | as a content wheel: reported when eligible, else local scrollback |
|
||||
|
||||
**A content-originated gesture terminates normally, at its last valid
|
||||
content coordinate.** That is the whole of the crossing rule, and it is
|
||||
what keeps a reporting child from being told about a row it does not
|
||||
own while still receiving its release.
|
||||
|
||||
#### Q#BP-R2 — a chrome wheel over a TERMINAL panel: clamp, or drop? **RULED: clamp**
|
||||
|
||||
The one product call inside R-c, flagged because it **diverges from the
|
||||
TUI deliberately**. The TUI drops terminal mode-line wheels; this rules
|
||||
that a panel clamps them into content instead.
|
||||
|
||||
The reason is that the panel's mode line is not a window's mode line
|
||||
among many — it is **the band's own chrome**, one row, and the document
|
||||
panel already scrolls on a chrome wheel (the TUI does not guard the
|
||||
wheel for documents). Dropping for terminals would make **the same
|
||||
pixel behave differently depending on the buffer kind currently shown
|
||||
there**, which is not a distinction a user is holding in mind while
|
||||
scrolling. **Overrule this if you would rather have TUI parity**; it is
|
||||
a one-line difference in the clamp and nothing else in R-c depends on
|
||||
it.
|
||||
|
||||
**Both crossings need witnesses, and they fail in opposite
|
||||
directions:**
|
||||
|
||||
- **Mode line → content:** press on the mode line, drag into content.
|
||||
- **Chrome → content:** press on the mode line, drag into content.
|
||||
**No `Drag` reaches the daemon**, because nothing armed. *Mutation:
|
||||
arm on a mode-line press — the orphan appears.*
|
||||
- **Content → mode line:** press in content, release over the mode
|
||||
line. **The gesture terminates** — the selection is committed, the
|
||||
latch clears. *Mutation: reserve `Up` as well — the gesture hangs,
|
||||
latched, and the next unrelated motion continues a selection the user
|
||||
ended.*
|
||||
arm on a chrome press — the orphan appears.*
|
||||
- **Content → chrome:** press in content, release over the mode line.
|
||||
**The gesture terminates at the last content coordinate.** For a
|
||||
**document** panel the selection commits and the latch clears; for a
|
||||
**reporting terminal** the child receives the release **at the
|
||||
content row**. *Mutation: reserve `Up` for documents — the gesture
|
||||
hangs latched; pass the raw chrome coordinate for terminals — the
|
||||
child receives no `Up` at all, because the bounds check drops it into
|
||||
the local branch.*
|
||||
|
||||
**The wheel is ruled explicitly**: a wheel over the mode line scrolls
|
||||
the panel exactly as one over content does. It is not a click, it
|
||||
carries no position semantics, and the TUI does not guard it.
|
||||
#### R-c2. The producer does not remember where the gesture began
|
||||
|
||||
`set_panel_pointer_held` **clears `last_pointer_cell`**
|
||||
(`pmacs-gpu/src/main.rs:7250`), and only `panel_motion_is_new`
|
||||
(`:7238`) ever fills it. `panel_release_cell` falls back to that field
|
||||
when the release is not over a cell (`:7225`–`:7230`).
|
||||
|
||||
Today that fallback is rarely reached, because chrome *is* a
|
||||
`PanelCell`. **Once R-c stops chrome being one, it becomes the normal
|
||||
path** — and it is empty. A `Down` in content followed **immediately**
|
||||
by a release over chrome or outside the band, with **no intervening
|
||||
motion**, has no coordinate to fall back to, so the `Up` is either
|
||||
dropped or sent with nothing.
|
||||
|
||||
**Retain the `Down` cell when arming.** Row: `Down` in content →
|
||||
release over chrome, **no intervening motion** → the `Up` carries the
|
||||
**`Down` cell's** coordinate. *Mutation: keep clearing the cell on arm
|
||||
— the release has no coordinate.* **For a reporting terminal the row
|
||||
asserts the exact child release BYTES**, not merely that the frontend
|
||||
latch cleared: a latch that clears while the child never hears the
|
||||
release is the failure this row exists for.
|
||||
|
||||
#### R-d. Panel replacement leaves the frontend's gesture latch armed
|
||||
|
||||
|
|
@ -2104,6 +2232,15 @@ unconditionally: an **ordinary same-identity frame refresh must NOT
|
|||
cancel a live gesture**. A panel repaints constantly during a drag;
|
||||
resetting on each frame would make selection impossible.
|
||||
|
||||
**D4's frame must be a CHANGED frame with unchanged epochs**, and the
|
||||
natural one is **the focus repaint the `Down` itself causes** — the
|
||||
panel activates, its mode line and cursor render differently, and the
|
||||
frame arrives mid-gesture with both epochs identical. A byte-identical
|
||||
duplicate would not exercise the rule at all: production returns early
|
||||
on one, *"A duplicate does no work — not even a reshape"*
|
||||
(`pmacs-gpu/src/main.rs:6918`–`:6919`), so the reset code is never
|
||||
reached and D4 passes against a broken implementation.
|
||||
|
||||
R-d's orphan is distinct from R-b's: **R-b's arrives from a passive
|
||||
panel, R-d's from a replaced or re-declared one**, and an
|
||||
implementation can fix either alone.
|
||||
|
|
|
|||
Loading…
Reference in New Issue