fix(gui-1b): B4 --- witness the production effect, drop the unframed fallback

TWO MUTATIONS LEFT BOTH B4 ROWS GREEN. Changing the source to
`Clipboard`, or replacing the dispatch arm with a no-op, was invisible:
one row asserted `middle_click_paste_source` in isolation and the other
asserted `route_pointer` in isolation, and nothing asserted the effect
the gesture produces. Two seams tested separately are not a path tested
once.

A third row drives a middle press through `dispatch_window_event` and
asserts EXACTLY ONE outbound `Paste` carrying the PRIMARY payload, and
that the release sends none. PRIMARY and CLIPBOARD are stubbed with
DISTINGUISHABLE contents, which is the point --- identical stubs would
pass with the wrong selection read.

That needed a seam: `State::set_test_selection`, consulted by
`read_os_selection` before the OS clipboard. A test-only field in
production code is a cost, and it is the smallest one that makes B4's
actual contract --- WHICH selection --- assertable without a real
clipboard. Both mutations now fire: source-to-Clipboard fires two rows,
the no-op dispatch fires the end-to-end row.

AND THE OFF-LINUX FALLBACK WAS UNFRAMED BEHAVIOUR I INVENTED. B4 rules
"PRIMARY on Linux" and rules nothing else. The gesture was inert on
every other platform; my previous commit made it paste the CLIPBOARD
there, and the row adopted that choice permanently. `middle_click_paste_source`
now returns `Option`, `None` off Linux, and the gesture stays inert. A
fallback needs framing and re-approval, not a default chosen while
implementing.
This commit is contained in:
Levi Neuwirth 2026-09-01 12:14:16 +02:00
parent 493db8f965
commit 3a4a748316
No known key found for this signature in database
1 changed files with 98 additions and 14 deletions

View File

@ -1873,6 +1873,9 @@ struct State {
/// caret follow leaves it alone until the cursor position actually /// caret follow leaves it alone until the cursor position actually
/// changes. A move fully absorbed by the clamp arms nothing. /// changes. A move fully absorbed by the clamp arms nothing.
manual_left_authority: bool, manual_left_authority: bool,
/// Stub selections for tests; see [`Self::set_test_selection`].
#[cfg(test)]
test_selections: HashMap<PasteSource, Vec<u8>>,
/// GUI Stage 1b B1: per-target, per-axis fractional wheel residual. /// GUI Stage 1b B1: per-target, per-axis fractional wheel residual.
/// Sub-tick deltas are banked here instead of being rounded away /// Sub-tick deltas are banked here instead of being rounded away
/// before routing knows where they were going. /// before routing knows where they were going.
@ -3429,7 +3432,9 @@ impl App {
/// it as a `Paste`, the same wire operation Ctrl-V uses. The daemon /// it as a `Paste`, the same wire operation Ctrl-V uses. The daemon
/// inserts it; this frontend never edits the document itself. /// inserts it; this frontend never edits the document itself.
fn apply_middle_press(&mut self) { fn apply_middle_press(&mut self) {
let source = middle_click_paste_source(); let Some(source) = middle_click_paste_source() else {
return;
};
let bytes = self let bytes = self
.state .state
.as_mut() .as_mut()
@ -4968,6 +4973,61 @@ mod input_routing_tests {
/// P2 — a button the frontend has no semantics for reaches no body: /// P2 — a button the frontend has no semantics for reaches no body:
/// nothing local, nothing outbound. The counterpart to the routing /// nothing local, nothing outbound. The counterpart to the routing
/// B4 END TO END — a middle press driven through
/// `dispatch_window_event` sends **exactly one** `Paste`, carrying
/// the **PRIMARY** payload, and its release sends none.
///
/// The two seam rows below cannot see this: mutating
/// `middle_click_paste_source` or replacing the dispatch arm with a
/// no-op leaves both of them green, because each asserts a function
/// in isolation rather than the effect the gesture produces. This
/// row drives the production path and asserts the payload.
///
/// The two selections carry **distinguishable** contents, which is
/// the whole point — a row whose PRIMARY and CLIPBOARD stubs said
/// the same thing would pass with the wrong one read.
#[test]
#[cfg(target_os = "linux")]
fn b4_a_middle_press_sends_exactly_one_paste_carrying_primary() {
use winit::event::{ElementState, MouseButton};
let mut h = EffectHarness::new();
if let Some(state) = h.app.state.as_mut() {
state.set_test_selection(crate::PasteSource::Primary, b"PRIMARY-payload");
state.set_test_selection(crate::PasteSource::Clipboard, b"CLIPBOARD-payload");
}
let step = h.feed(&mouse_input(ElementState::Pressed, MouseButton::Middle));
let pastes: Vec<&pmacs_protocol::FrontendEvent> = step
.outbound
.iter()
.filter(|e| matches!(e, pmacs_protocol::FrontendEvent::Paste { .. }))
.collect();
assert_eq!(
pastes.len(),
1,
"exactly one paste, got {:?}",
step.outbound
);
match pastes[0] {
pmacs_protocol::FrontendEvent::Paste { data, .. } => assert_eq!(
data.as_slice(),
b"PRIMARY-payload",
"B4 pastes the PRIMARY selection, not the clipboard"
),
other => panic!("expected a Paste, got {other:?}"),
}
let release = h.feed(&mouse_input(ElementState::Released, MouseButton::Middle));
assert!(
!release
.outbound
.iter()
.any(|e| matches!(e, pmacs_protocol::FrontendEvent::Paste { .. })),
"the release pastes nothing; the gesture fires once, on the press"
);
}
/// row that calls it claimed-and-dropped. /// row that calls it claimed-and-dropped.
#[test] #[test]
fn an_unused_button_produces_no_effect_of_any_kind() { fn an_unused_button_produces_no_effect_of_any_kind() {
@ -5333,7 +5393,7 @@ const WHEEL_COLUMNS_PER_TICK: f32 = 3.0;
/// copy, and the PRIMARY selection, written merely by selecting text. /// copy, and the PRIMARY selection, written merely by selecting text.
/// **They are different selections with different contents**, and the /// **They are different selections with different contents**, and the
/// platform convention pairs them with different gestures. /// platform convention pairs them with different gestures.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
enum PasteSource { enum PasteSource {
/// What Ctrl-V reads. /// What Ctrl-V reads.
Clipboard, Clipboard,
@ -5341,16 +5401,20 @@ enum PasteSource {
Primary, Primary,
} }
/// The selection a middle click pastes from. /// The selection a middle click pastes from, or `None` where the
/// gesture has no ruled meaning.
/// ///
/// **PRIMARY on Linux** — B4's whole content. Elsewhere there is no /// **B4 rules PRIMARY on Linux, and rules nothing else.** Off Linux
/// PRIMARY selection, so the gesture falls back to the clipboard rather /// there is no PRIMARY selection, and the gesture was inert before this
/// than doing nothing, which is the closest available meaning. /// slice; making it paste the CLIPBOARD instead would be a new
const fn middle_click_paste_source() -> PasteSource { /// behaviour on every other platform that no framing approved. It stays
/// inert, and a fallback needs framing and re-approval rather than a
/// default chosen here.
const fn middle_click_paste_source() -> Option<PasteSource> {
if cfg!(target_os = "linux") { if cfg!(target_os = "linux") {
PasteSource::Primary Some(PasteSource::Primary)
} else { } else {
PasteSource::Clipboard None
} }
} }
@ -5959,6 +6023,8 @@ impl State {
last_pointer_down: None, last_pointer_down: None,
minimap_scrub_active: false, minimap_scrub_active: false,
manual_left_authority: false, manual_left_authority: false,
#[cfg(test)]
test_selections: HashMap::new(),
wheel_residuals: WheelResiduals::default(), wheel_residuals: WheelResiduals::default(),
edge_scroll_dir: None, edge_scroll_dir: None,
edge_scroll_last: None, edge_scroll_last: None,
@ -6444,6 +6510,20 @@ impl State {
self.read_os_selection(PasteSource::Clipboard) self.read_os_selection(PasteSource::Clipboard)
} }
/// Stub selection contents for tests, consulted by
/// [`Self::read_os_selection`] before the OS clipboard.
///
/// **A test seam in production code, and deliberately so.** B4's
/// contract is *which selection* a middle click reads, and the two
/// selections cannot be told apart through a real clipboard in a
/// unit test — a row that asserts "a paste happened" passes with the
/// wrong selection read. This is the smallest seam that lets the row
/// assert the payload rather than the seam that chose it.
#[cfg(test)]
fn set_test_selection(&mut self, source: PasteSource, bytes: &[u8]) {
self.test_selections.insert(source, bytes.to_vec());
}
/// Read one named OS selection. /// Read one named OS selection.
/// ///
/// GUI Stage 1b B4 needs the **PRIMARY** selection, which on Linux /// GUI Stage 1b B4 needs the **PRIMARY** selection, which on Linux
@ -6453,6 +6533,10 @@ impl State {
/// selected — a plausible-looking wrong answer, which is why B4's /// selected — a plausible-looking wrong answer, which is why B4's
/// row asserts the source rather than that "a paste happened". /// row asserts the source rather than that "a paste happened".
fn read_os_selection(&mut self, source: PasteSource) -> Option<Vec<u8>> { fn read_os_selection(&mut self, source: PasteSource) -> Option<Vec<u8>> {
#[cfg(test)]
if let Some(bytes) = self.test_selections.get(&source) {
return Some(bytes.clone());
}
let clipboard = self.os_clipboard()?; let clipboard = self.os_clipboard()?;
let read = match source { let read = match source {
PasteSource::Clipboard => clipboard.get_text(), PasteSource::Clipboard => clipboard.get_text(),
@ -14718,15 +14802,15 @@ mod tests {
if cfg!(target_os = "linux") { if cfg!(target_os = "linux") {
assert_eq!( assert_eq!(
source, source,
super::PasteSource::Primary, Some(super::PasteSource::Primary),
"B4: the middle button reads PRIMARY on Linux" "B4: the middle button reads PRIMARY on Linux"
); );
} else { } else {
assert_eq!( assert_eq!(
source, source, None,
super::PasteSource::Clipboard, "B4 rules PRIMARY on Linux and nothing else; off Linux the \
"off Linux there is no PRIMARY selection; the clipboard is \ gesture stays inert rather than acquiring an unframed \
the closest available meaning" clipboard meaning"
); );
} }
} }