fix(gpu): present horizontal wheel and reset replaced panels
Request a frame whenever frontend-local horizontal scrolling changes the GPU origin, so the waiting event loop presents the new viewport without unrelated input. Discard panel wheel residuals on accepted mapped and legacy identity replacements, while preserving them across a geometry-only re-grid of the same panel. Add production-path witnesses for the redraw and both frame families, plus the geometry negative control. Refresh the Stage 1b framing as revision 22 with the review findings and their discriminators.
This commit is contained in:
parent
c4b1505179
commit
ef2592020d
|
|
@ -1,13 +1,30 @@
|
||||||
# GUI arc, Stage 1 — input foundation (framing)
|
# GUI arc, Stage 1 — input foundation (framing)
|
||||||
|
|
||||||
**Status: revision 21 — revision 20's §2a re-measurement at `0ec13b3`
|
**Status: revision 22 — IMPLEMENTED, IN REVIEW as PR #247. Revision
|
||||||
stands; the GPU lifetime rows now describe structural authority rather
|
21's GPU structural-authority correction stands; the first PR review
|
||||||
than a latch that never had a reader.** Revision 18's §2a was measured
|
closed one presentation gap and one incomplete disposal path without
|
||||||
|
changing a ruling.** Revision 18's §2a was measured
|
||||||
at `72da24a`. That
|
at `72da24a`. That
|
||||||
base is now **167 commits back**, and #242 and #243 landed on exactly
|
base is now **167 commits back**, and #242 and #243 landed on exactly
|
||||||
the pointer paths 1b builds on, so every 1b anchor is stale again —
|
the pointer paths 1b builds on, so every 1b anchor is stale again —
|
||||||
which §2a's own history predicted would happen.
|
which §2a's own history predicted would happen.
|
||||||
|
|
||||||
|
**Revision 22 closes two review findings in the implementation and in
|
||||||
|
the evidence.** First, the GPU's document-horizontal wheel changed
|
||||||
|
`code_scroll_left` but requested no redraw. The event loop returns to
|
||||||
|
`ControlFlow::Wait`, and this axis deliberately sends no viewport wire
|
||||||
|
event, so the stored origin could remain invisible until an unrelated
|
||||||
|
frame. B2 now includes a request-count witness whose origin assertion
|
||||||
|
still passes when the request is removed — the redraw is the
|
||||||
|
discriminator, not inferred from state. Second, B1's panel disposal ran
|
||||||
|
on `Absent` only. A direct accepted `Present` → `Present` replacement
|
||||||
|
could retain panel A's bank; A → B → A could later spend it. Separate
|
||||||
|
mapped and legacy rows drive the real wheel producer across a same-buffer,
|
||||||
|
new-`panel_epoch` replacement with no `Absent`. A geometry-only re-grid
|
||||||
|
is the negative control: it is still the same panel surface and keeps
|
||||||
|
the fraction. **No ruling changes; two paths that failed to implement
|
||||||
|
the existing rulings are now covered.**
|
||||||
|
|
||||||
**All four load-bearing claims were re-verified and all four SURVIVE:**
|
**All four load-bearing claims were re-verified and all four SURVIVE:**
|
||||||
|
|
||||||
1. **Quantization still happens BEFORE routing.** `:3254` quantizes,
|
1. **Quantization still happens BEFORE routing.** `:3254` quantizes,
|
||||||
|
|
@ -497,6 +514,13 @@ defect as leaking between kinds but invisible to a kind-level row.
|
||||||
with panel B (the `buffer_id`/`panel_epoch` replacement acceptance 49
|
with panel B (the `buffer_id`/`panel_epoch` replacement acceptance 49
|
||||||
already models), motion over B must **start from zero**. *Mutation:
|
already models), motion over B must **start from zero**. *Mutation:
|
||||||
key the residual by "the panel" rather than by panel identity.*
|
key the residual by "the panel" rather than by panel identity.*
|
||||||
|
**Revision 22 adds the disposal discriminator R2 alone could not
|
||||||
|
supply:** A starting from zero is satisfied by `BufferId` keying even
|
||||||
|
while A's stale bank remains. Drive a direct accepted A → B → A (or
|
||||||
|
same-buffer/new-`panel_epoch`) replacement with no `Absent`; A's first
|
||||||
|
successor half-notch does nothing and its second completes. Run it in
|
||||||
|
both mapped and legacy frame families. A geometry-only successor is a
|
||||||
|
negative control and preserves the bank.
|
||||||
- **R3 — terminal A→terminal B:** the same, across two terminal
|
- **R3 — terminal A→terminal B:** the same, across two terminal
|
||||||
buffers. *Mutation: key the residual by "the terminal".*
|
buffers. *Mutation: key the residual by "the terminal".*
|
||||||
|
|
||||||
|
|
@ -1214,7 +1238,7 @@ The crate has **exactly one** executable `event_loop.exit()`, in
|
||||||
| # | Contract | Witness | Mutation |
|
| # | Contract | Witness | Mutation |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 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 |
|
| 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 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 |
|
| 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; omit the GPU local redraw request → `code_scroll_left` changes while the waiting event loop never presents it |
|
||||||
| 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 |
|
| 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 |
|
||||||
|
|
|
||||||
|
|
@ -1636,6 +1636,12 @@ struct State {
|
||||||
/// on a windowless `State`. Test-only.
|
/// on a windowless `State`. Test-only.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
render_calls: u64,
|
render_calls: u64,
|
||||||
|
/// How many redraws production requested. Test-only: a headless
|
||||||
|
/// `State` has no [`Window`] to retain the request, so without this
|
||||||
|
/// counter a row can see viewport state change while missing that the
|
||||||
|
/// live event loop will remain asleep and never present it.
|
||||||
|
#[cfg(test)]
|
||||||
|
redraw_requests: std::cell::Cell<u64>,
|
||||||
// `None` in the headless render-test path (F-014): a windowless State
|
// `None` in the headless render-test path (F-014): a windowless State
|
||||||
// that renders to an offscreen texture instead of a surface.
|
// that renders to an offscreen texture instead of a surface.
|
||||||
window: Option<Arc<Window>>,
|
window: Option<Arc<Window>>,
|
||||||
|
|
@ -2204,12 +2210,14 @@ impl WheelTarget {
|
||||||
/// away must go away with it, or it is spent on whatever later takes
|
/// away must go away with it, or it is spent on whatever later takes
|
||||||
/// that identity.
|
/// that identity.
|
||||||
///
|
///
|
||||||
/// **Disposal is implemented at both teardowns**, because the key
|
/// **Disposal is implemented at both teardowns and at accepted panel
|
||||||
/// alone cannot see a surface closed and REOPENED on the same buffer:
|
/// replacement.** The key alone cannot see a surface closed and REOPENED
|
||||||
/// the successor carries the same `BufferId`, so nothing distinguishes
|
/// on the same buffer: the successor carries the same `BufferId`, so
|
||||||
/// it from the surface the user was actually scrolling.
|
/// nothing distinguishes it from the surface the user was actually
|
||||||
/// `PanelFramePayload::Absent` clears the panel banks and
|
/// scrolling. `PanelFramePayload::Absent` clears the panel banks,
|
||||||
/// `exit_terminal_mode` clears the terminal ones.
|
/// accepted `Present`/`PresentMapped` identity replacements clear them
|
||||||
|
/// without waiting for an `Absent`, and `exit_terminal_mode` clears the
|
||||||
|
/// terminal ones.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct WheelResiduals {
|
struct WheelResiduals {
|
||||||
/// `(owner) -> (x, y)` in fractional ticks, each in `(-1.0, 1.0)`.
|
/// `(owner) -> (x, y)` in fractional ticks, each in `(-1.0, 1.0)`.
|
||||||
|
|
@ -5138,6 +5146,66 @@ mod input_routing_tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// B2, GPU presentation half — changing the frontend-local horizontal
|
||||||
|
/// origin requests the frame that makes the change visible.
|
||||||
|
///
|
||||||
|
/// The event loop sleeps in `ControlFlow::Wait`, and this axis sends no
|
||||||
|
/// viewport message that could provoke a daemon frame. Inspecting
|
||||||
|
/// `code_scroll_left` alone therefore cannot distinguish a working
|
||||||
|
/// gesture from one whose new origin remains invisible until some
|
||||||
|
/// unrelated repaint.
|
||||||
|
///
|
||||||
|
/// *Mutation: omit `request_redraw()` from `scroll_by_columns` → this
|
||||||
|
/// row. The origin assertion still passes, proving the redraw counter is
|
||||||
|
/// the discriminator rather than corroboration.*
|
||||||
|
#[test]
|
||||||
|
fn b2_a_gpu_horizontal_wheel_requests_the_frame_that_displays_it() {
|
||||||
|
let mut h = EffectHarness::with_document(&format!("{}\n", "wide ".repeat(120)).repeat(200));
|
||||||
|
{
|
||||||
|
let buffer_id = h
|
||||||
|
.app
|
||||||
|
.state
|
||||||
|
.as_ref()
|
||||||
|
.expect("harness state")
|
||||||
|
.current_buffer_id
|
||||||
|
.expect("the harness stands in a buffer");
|
||||||
|
let state = h.app.state.as_mut().expect("harness state");
|
||||||
|
let _ = state.apply_attach_message(InstanceMessage::LineWrapFacts {
|
||||||
|
buffer_id,
|
||||||
|
wrap: false,
|
||||||
|
});
|
||||||
|
assert_eq!(state.buffer.wrap(), Wrap::None, "setup: wrap is off");
|
||||||
|
}
|
||||||
|
let document = document_probe(&h);
|
||||||
|
move_pointer(&mut h, document);
|
||||||
|
assert_eq!(
|
||||||
|
h.app.classify_wheel_target(document.0, document.1),
|
||||||
|
WheelTarget::Document,
|
||||||
|
"setup: document text"
|
||||||
|
);
|
||||||
|
let state = h.app.state.as_ref().expect("state");
|
||||||
|
let left_before = state.code_scroll_left;
|
||||||
|
let redraws_before = state.redraw_requests.get();
|
||||||
|
|
||||||
|
let step = h.feed(&wheel(1.0, 0.0));
|
||||||
|
|
||||||
|
let state = h.app.state.as_ref().expect("state");
|
||||||
|
assert!(
|
||||||
|
state.code_scroll_left > left_before,
|
||||||
|
"setup: the wheel must change the origin, or a redraw is not owed"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.redraw_requests.get(),
|
||||||
|
redraws_before + 1,
|
||||||
|
"the local-only origin change must wake the waiting event loop"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
step.outbound.is_empty(),
|
||||||
|
"the redraw is locally requested, not induced by wire traffic: {:?}",
|
||||||
|
step.outbound
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Replace the harness's document with a fresh buffer, through the
|
/// Replace the harness's document with a fresh buffer, through the
|
||||||
/// production `BufferSnapshot` receiver.
|
/// production `BufferSnapshot` receiver.
|
||||||
fn replace_the_buffer(h: &mut EffectHarness) {
|
fn replace_the_buffer(h: &mut EffectHarness) {
|
||||||
|
|
@ -6548,6 +6616,8 @@ impl State {
|
||||||
let mut state = Self {
|
let mut state = Self {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
render_calls: 0,
|
render_calls: 0,
|
||||||
|
#[cfg(test)]
|
||||||
|
redraw_requests: std::cell::Cell::new(0),
|
||||||
window,
|
window,
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
|
|
@ -8320,6 +8390,13 @@ impl State {
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// B1's disposal half applies to a direct accepted
|
||||||
|
// replacement too, not only to `Absent`. The bank key is
|
||||||
|
// the buffer id, so without this a panel A -> B -> A
|
||||||
|
// sequence can spend A's pre-replacement fraction when A
|
||||||
|
// returns. Run only after every refusal/duplicate check:
|
||||||
|
// an unaccepted successor owns no state to reset.
|
||||||
|
self.discard_replaced_panel_wheel_residual(&frame);
|
||||||
// NOTE: the gesture-latch reset on an identity change is
|
// NOTE: the gesture-latch reset on an identity change is
|
||||||
// R-d, owned by `panel-pointer-replay`. It is not
|
// R-d, owned by `panel-pointer-replay`. It is not
|
||||||
// duplicated here — two branches resetting the same
|
// duplicated here — two branches resetting the same
|
||||||
|
|
@ -8375,6 +8452,11 @@ impl State {
|
||||||
self.panel.last_pointer_cell = None;
|
self.panel.last_pointer_cell = None;
|
||||||
self.panel.gesture_last_content_cell = None;
|
self.panel.gesture_last_content_cell = None;
|
||||||
}
|
}
|
||||||
|
// The pointer latch above includes a geometry change; the
|
||||||
|
// wheel bank does not. A fractional notch belongs to the
|
||||||
|
// panel presentation and survives a re-grid of that same
|
||||||
|
// panel, but never a panel/buffer replacement.
|
||||||
|
self.discard_replaced_panel_wheel_residual(&frame);
|
||||||
let plan = TerminalPaintPlan::build_grid(
|
let plan = TerminalPaintPlan::build_grid(
|
||||||
frame.size,
|
frame.size,
|
||||||
&frame.cells,
|
&frame.cells,
|
||||||
|
|
@ -8389,6 +8471,22 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drop panel wheel state when an accepted frame replaces the panel
|
||||||
|
/// presentation that owned it.
|
||||||
|
///
|
||||||
|
/// `BufferId` alone is not an identity: a persistent buffer can leave
|
||||||
|
/// and later return as a new `panel_epoch`. Conversely, a geometry-only
|
||||||
|
/// change leaves the same scroll surface in place, so it does not spend
|
||||||
|
/// or discard a fractional notch.
|
||||||
|
fn discard_replaced_panel_wheel_residual(&mut self, successor: &PanelFrame) {
|
||||||
|
let replaced = self.panel.frame.as_ref().is_some_and(|current| {
|
||||||
|
current.buffer_id != successor.buffer_id || current.panel_epoch != successor.panel_epoch
|
||||||
|
});
|
||||||
|
if replaced {
|
||||||
|
self.wheel_residuals.clear_panels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Reshape one cosmic-text buffer per planned panel run.
|
/// Reshape one cosmic-text buffer per planned panel run.
|
||||||
///
|
///
|
||||||
/// One buffer per RUN for the same reason terminal mode does it: a
|
/// One buffer per RUN for the same reason terminal mode does it: a
|
||||||
|
|
@ -9284,7 +9382,11 @@ impl State {
|
||||||
/// frontends are not required to share a representation.
|
/// frontends are not required to share a representation.
|
||||||
fn scroll_by_columns(&mut self, columns: i64) {
|
fn scroll_by_columns(&mut self, columns: i64) {
|
||||||
if self.buffer.wrap() != Wrap::None {
|
if self.buffer.wrap() != Wrap::None {
|
||||||
|
let changed = self.code_scroll_left != 0.0;
|
||||||
self.code_scroll_left = 0.0;
|
self.code_scroll_left = 0.0;
|
||||||
|
if changed {
|
||||||
|
self.request_redraw();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let advance = self.mono_advance();
|
let advance = self.mono_advance();
|
||||||
|
|
@ -9300,6 +9402,11 @@ impl State {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.code_scroll_left = next as f32 * advance;
|
self.code_scroll_left = next as f32 * advance;
|
||||||
|
// Unlike a vertical wheel, this path emits no Viewport and
|
||||||
|
// rebuilds no lines. Nothing else wakes the `ControlFlow::Wait`
|
||||||
|
// event loop, so changing the origin without requesting a frame
|
||||||
|
// leaves the new viewport invisible until an unrelated redraw.
|
||||||
|
self.request_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move `code_scroll_left` so the caret's column is on screen
|
/// Move `code_scroll_left` so the caret's column is on screen
|
||||||
|
|
@ -11264,6 +11371,9 @@ impl State {
|
||||||
/// Ask the window to repaint. A no-op headless (no window), where the
|
/// Ask the window to repaint. A no-op headless (no window), where the
|
||||||
/// render tests drive `render_offscreen` directly (F-014).
|
/// render tests drive `render_offscreen` directly (F-014).
|
||||||
fn request_redraw(&self) {
|
fn request_redraw(&self) {
|
||||||
|
#[cfg(test)]
|
||||||
|
self.redraw_requests
|
||||||
|
.set(self.redraw_requests.get().saturating_add(1));
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
|
@ -22430,6 +22540,230 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Exercise B1's panel-disposal rule across a direct, accepted
|
||||||
|
/// `Present` -> `Present` identity replacement, with no intervening
|
||||||
|
/// `Absent` to clear the bank for us.
|
||||||
|
///
|
||||||
|
/// The same persistent buffer returns under a new `panel_epoch`, which
|
||||||
|
/// is the case `ResidualOwner::Panel(BufferId)` cannot distinguish. A
|
||||||
|
/// first successor half-notch must do nothing; a second must still
|
||||||
|
/// complete, so the row cannot pass against an accumulator that banks
|
||||||
|
/// nothing.
|
||||||
|
fn install_replacement_test_panel(h: &mut EffectHarness, mapped: bool) {
|
||||||
|
let state = h.app.state.as_mut().expect("harness state");
|
||||||
|
state.set_panel_wire(if mapped {
|
||||||
|
pmacs_protocol::PANEL_MAPPING_MIN_VERSION
|
||||||
|
} else {
|
||||||
|
PANEL_MIN_VERSION
|
||||||
|
});
|
||||||
|
let (geometry_epoch, total) = state
|
||||||
|
.next_geometry_declaration(GeometryTrigger::Metrics)
|
||||||
|
.expect("metrics advance the panel geometry");
|
||||||
|
let frame = panel_frame_of_buffer(
|
||||||
|
BufferId::from_raw(77),
|
||||||
|
4,
|
||||||
|
total.cols.max(1),
|
||||||
|
geometry_epoch,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
let payload = if mapped {
|
||||||
|
PanelFramePayload::PresentMapped {
|
||||||
|
frame,
|
||||||
|
mapping_generation: 1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PanelFramePayload::Present(frame)
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
state.apply_panel_payload(payload),
|
||||||
|
"setup: the initial {} frame must be accepted",
|
||||||
|
if mapped { "mapped" } else { "legacy" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_at_replacement_test_panel(h: &mut EffectHarness) {
|
||||||
|
use winit::dpi::PhysicalPosition;
|
||||||
|
use winit::event::DeviceId;
|
||||||
|
let (px, py, _, ph) = h
|
||||||
|
.app
|
||||||
|
.state
|
||||||
|
.as_ref()
|
||||||
|
.expect("harness state")
|
||||||
|
.panel_content_rect()
|
||||||
|
.expect("the panel is presented");
|
||||||
|
let point = (f64::from(px + 4.0), f64::from(py + ph / 2.0));
|
||||||
|
h.feed(&WindowEvent::CursorMoved {
|
||||||
|
device_id: DeviceId::dummy(),
|
||||||
|
position: PhysicalPosition::new(point.0, point.1),
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
h.app.classify_wheel_target(point.0, point.1),
|
||||||
|
WheelTarget::PanelCell { .. }
|
||||||
|
),
|
||||||
|
"setup: the probe must be a panel cell"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn panel_half_notch(h: &mut EffectHarness) -> Step {
|
||||||
|
use winit::event::{DeviceId, MouseScrollDelta, TouchPhase};
|
||||||
|
|
||||||
|
h.feed(&WindowEvent::MouseWheel {
|
||||||
|
device_id: DeviceId::dummy(),
|
||||||
|
delta: MouseScrollDelta::LineDelta(0.0, -0.6),
|
||||||
|
phase: TouchPhase::Moved,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn panel_gesture_count(step: &Step) -> usize {
|
||||||
|
step.outbound
|
||||||
|
.iter()
|
||||||
|
.filter(|event| {
|
||||||
|
matches!(
|
||||||
|
event,
|
||||||
|
pmacs_protocol::FrontendEvent::PanelPointer { .. }
|
||||||
|
| pmacs_protocol::FrontendEvent::PanelPointerMapped { .. }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_test_panel_directly(h: &mut EffectHarness, mapped: bool) {
|
||||||
|
let state = h.app.state.as_mut().expect("harness state");
|
||||||
|
let mut successor = state.panel.frame.clone().expect("retained frame");
|
||||||
|
successor.panel_epoch += 1;
|
||||||
|
let payload = if mapped {
|
||||||
|
PanelFramePayload::PresentMapped {
|
||||||
|
frame: successor,
|
||||||
|
mapping_generation: 1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PanelFramePayload::Present(successor)
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
state.apply_panel_payload(payload),
|
||||||
|
"the direct identity replacement must be accepted"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.panel.frame.as_ref().map(|frame| frame.panel_epoch),
|
||||||
|
Some(2),
|
||||||
|
"setup: the successor, not the predecessor, is retained"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_panel_replacement_discards_wheel_residual(mapped: bool) {
|
||||||
|
let mut h = EffectHarness::new();
|
||||||
|
install_replacement_test_panel(&mut h, mapped);
|
||||||
|
point_at_replacement_test_panel(&mut h);
|
||||||
|
|
||||||
|
let step = panel_half_notch(&mut h);
|
||||||
|
assert_eq!(
|
||||||
|
panel_gesture_count(&step),
|
||||||
|
0,
|
||||||
|
"setup: the predecessor banks 0.6 and emits nothing"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Direct replacement: no `Absent`, no pointer motion and the same
|
||||||
|
// buffer id. Only the accepted successor identity can discard the
|
||||||
|
// predecessor's bank.
|
||||||
|
replace_test_panel_directly(&mut h, mapped);
|
||||||
|
|
||||||
|
let step = panel_half_notch(&mut h);
|
||||||
|
assert_eq!(
|
||||||
|
panel_gesture_count(&step),
|
||||||
|
0,
|
||||||
|
"the successor starts from zero; the predecessor's 0.6 is gone: {:?}",
|
||||||
|
step.outbound
|
||||||
|
);
|
||||||
|
let step = panel_half_notch(&mut h);
|
||||||
|
assert_eq!(
|
||||||
|
panel_gesture_count(&step),
|
||||||
|
1,
|
||||||
|
"the successor's own 0.6 + 0.6 still completes exactly once: {:?}",
|
||||||
|
step.outbound
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
step.outbound.last(),
|
||||||
|
Some(
|
||||||
|
pmacs_protocol::FrontendEvent::PanelPointer {
|
||||||
|
kind: pmacs_protocol::MouseKind::ScrollDown,
|
||||||
|
..
|
||||||
|
} | pmacs_protocol::FrontendEvent::PanelPointerMapped {
|
||||||
|
kind: pmacs_protocol::MouseKind::ScrollDown,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"the completed successor notch is downward: {:?}",
|
||||||
|
step.outbound
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current production sessions negotiate the mapped family.
|
||||||
|
///
|
||||||
|
/// *Mutation: omit the replacement discard from `PresentMapped` →
|
||||||
|
/// this row only.*
|
||||||
|
#[test]
|
||||||
|
fn b1_a_mapped_panel_replacement_discards_the_predecessors_residual() {
|
||||||
|
assert_panel_replacement_discards_wheel_residual(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The legacy family remains supported at the protocol floor.
|
||||||
|
///
|
||||||
|
/// *Mutation: omit the replacement discard from `Present` → this row
|
||||||
|
/// only.*
|
||||||
|
#[test]
|
||||||
|
fn b1_a_legacy_panel_replacement_discards_the_predecessors_residual() {
|
||||||
|
assert_panel_replacement_discards_wheel_residual(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A geometry declaration re-grids the same panel; it does not replace
|
||||||
|
/// the scroll surface that owns an in-progress fractional notch.
|
||||||
|
///
|
||||||
|
/// *Mutation: include `geometry_epoch` in
|
||||||
|
/// `discard_replaced_panel_wheel_residual`'s identity comparison → this
|
||||||
|
/// row. The second half-notch no longer completes.*
|
||||||
|
#[test]
|
||||||
|
fn b1_a_panel_geometry_change_preserves_the_same_panels_residual() {
|
||||||
|
let mut h = EffectHarness::new();
|
||||||
|
install_replacement_test_panel(&mut h, false);
|
||||||
|
point_at_replacement_test_panel(&mut h);
|
||||||
|
|
||||||
|
let step = panel_half_notch(&mut h);
|
||||||
|
assert_eq!(
|
||||||
|
panel_gesture_count(&step),
|
||||||
|
0,
|
||||||
|
"setup: the first 0.6 banks against the panel"
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
let state = h.app.state.as_mut().expect("harness state");
|
||||||
|
let (geometry_epoch, total) = state
|
||||||
|
.next_geometry_declaration(GeometryTrigger::Metrics)
|
||||||
|
.expect("metrics advance the geometry epoch");
|
||||||
|
let mut regridded = state.panel.frame.clone().expect("retained frame");
|
||||||
|
regridded.geometry_epoch = geometry_epoch;
|
||||||
|
regridded.size.cols = total.cols.max(1);
|
||||||
|
regridded.cells.resize(
|
||||||
|
regridded.size.rows as usize * regridded.size.cols as usize,
|
||||||
|
terminal_cell(pmacs_protocol::Glyph::Char('x'), CellStyle::default()),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
state.apply_panel_payload(PanelFramePayload::Present(regridded)),
|
||||||
|
"the matching frame must settle the new geometry"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let step = panel_half_notch(&mut h);
|
||||||
|
assert_eq!(
|
||||||
|
panel_gesture_count(&step),
|
||||||
|
1,
|
||||||
|
"the same panel keeps its fraction across a re-grid: {:?}",
|
||||||
|
step.outbound
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// B1's disposal half, **terminal side** — leaving a terminal and
|
/// B1's disposal half, **terminal side** — leaving a terminal and
|
||||||
/// re-entering **the same** one starts from zero.
|
/// re-entering **the same** one starts from zero.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue