feat(gui-1b): B4 --- a middle click pastes the PRIMARY selection
`PointerRoute::UnusedButton`'s own doc named this row: "Stage 1b's B4 gives the middle button a meaning (PRIMARY-selection paste on Linux) and lands here." B4 splits that variant, as §2a said it would. A middle PRESS is now `PointerRoute::MiddlePress` and reads the PRIMARY selection, shipping it as the same `Paste` wire operation Ctrl-V uses. Its RELEASE stays unused, like the right button's --- the paste happens once, on the press. PRIMARY and CLIPBOARD are different selections with different contents: the clipboard holds what was last explicitly copied, PRIMARY holds what is currently selected. Reading the wrong one still produces a paste, just not the one the platform convention promises, so the row asserts the SOURCE rather than that a paste happened. `middle_click_paste_source` is the seam that makes that assertable without an OS clipboard; `read_os_selection` takes the source and uses arboard's `GetExtLinux` for PRIMARY. Two rows, three mutations, each firing: source = Clipboard -> the source row middle press unrouted -> the routing row release also pastes -> the routing row Three existing rows encoded the old behaviour --- that a middle press is semantics-free. They are updated to keep testing what they SAY rather than being weakened to accommodate B4: the routing row now covers Back/Forward/Other plus the middle RELEASE, and the two effect/order rows switch to `Back`, a button that still has no semantics. Widening them to accept the new meaning would have left no row asserting that semantics-free buttons stay inert.
This commit is contained in:
parent
5371229b9a
commit
493db8f965
|
|
@ -3423,6 +3423,26 @@ impl App {
|
||||||
WheelTarget::Chrome
|
WheelTarget::Chrome
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform [`PointerRoute::MiddlePress`] — GUI Stage 1b B4.
|
||||||
|
///
|
||||||
|
/// Reads the selection [`middle_click_paste_source`] names and ships
|
||||||
|
/// it as a `Paste`, the same wire operation Ctrl-V uses. The daemon
|
||||||
|
/// inserts it; this frontend never edits the document itself.
|
||||||
|
fn apply_middle_press(&mut self) {
|
||||||
|
let source = middle_click_paste_source();
|
||||||
|
let bytes = self
|
||||||
|
.state
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|state| state.read_os_selection(source));
|
||||||
|
if let Some(bytes) = bytes
|
||||||
|
&& !bytes.is_empty()
|
||||||
|
&& let Some(client) = self.attach_client.as_ref()
|
||||||
|
&& let Err(e) = client.send_paste(bytes)
|
||||||
|
{
|
||||||
|
eprintln!("pmacs-gpu: middle-click send_paste failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform [`PointerRoute::Wheel`].
|
/// Perform [`PointerRoute::Wheel`].
|
||||||
///
|
///
|
||||||
/// **GUI Stage 1b B1 reorders this pipeline.** It used to round to
|
/// **GUI Stage 1b B1 reorders this pipeline.** It used to round to
|
||||||
|
|
@ -3578,6 +3598,7 @@ impl App {
|
||||||
// today nothing to do: a key-up the keyboard family claimed
|
// today nothing to do: a key-up the keyboard family claimed
|
||||||
// and dropped, a button the pointer family has no semantics
|
// and dropped, a button the pointer family has no semantics
|
||||||
// for, and an event no family claims at all.
|
// for, and an event no family claims at all.
|
||||||
|
Route::Pointer(PointerRoute::MiddlePress) => self.apply_middle_press(),
|
||||||
Route::Keyboard {
|
Route::Keyboard {
|
||||||
action: KeyAction::Release,
|
action: KeyAction::Release,
|
||||||
..
|
..
|
||||||
|
|
@ -4019,12 +4040,16 @@ enum PointerRoute {
|
||||||
/// opens on the press, so the matching release is deliberately
|
/// opens on the press, so the matching release is deliberately
|
||||||
/// nothing — see `UnusedButton`.
|
/// nothing — see `UnusedButton`.
|
||||||
RightPress,
|
RightPress,
|
||||||
/// A `MouseInput` this frontend has no semantics for: the right
|
/// `MouseInput` **pressing** the middle button — GUI Stage 1b's
|
||||||
/// button's release, and every middle / back / forward / other
|
/// B4. On Linux this pastes the **PRIMARY selection**, which is the
|
||||||
/// button in either state. **Claimed by the pointer family and
|
/// platform convention and a different selection from the one
|
||||||
/// dropped**, exactly as it behaved when it fell through to the
|
/// Ctrl-V reads. The release is deliberately nothing, like the
|
||||||
/// wildcard. Stage 1b's B4 gives the middle button a meaning
|
/// right button's.
|
||||||
/// (PRIMARY-selection paste on Linux) and lands here.
|
MiddlePress,
|
||||||
|
/// A `MouseInput` this frontend has no semantics for: the right and
|
||||||
|
/// middle buttons' releases, and every back / forward / other button
|
||||||
|
/// in either state. **Claimed by the pointer family and dropped**,
|
||||||
|
/// exactly as it behaved when it fell through to the wildcard.
|
||||||
UnusedButton,
|
UnusedButton,
|
||||||
/// `MouseWheel`. The delta is carried raw: converting it to lines
|
/// `MouseWheel`. The delta is carried raw: converting it to lines
|
||||||
/// needs the code line height, which is `State`'s to know.
|
/// needs the code line height, which is `State`'s to know.
|
||||||
|
|
@ -4042,6 +4067,7 @@ fn route_pointer(event: &WindowEvent) -> Option<PointerRoute> {
|
||||||
WindowEvent::MouseInput { state, button, .. } => Some(match (button, state) {
|
WindowEvent::MouseInput { state, button, .. } => Some(match (button, state) {
|
||||||
(MouseButton::Left, _) => PointerRoute::Left(*state),
|
(MouseButton::Left, _) => PointerRoute::Left(*state),
|
||||||
(MouseButton::Right, ElementState::Pressed) => PointerRoute::RightPress,
|
(MouseButton::Right, ElementState::Pressed) => PointerRoute::RightPress,
|
||||||
|
(MouseButton::Middle, ElementState::Pressed) => PointerRoute::MiddlePress,
|
||||||
_ => PointerRoute::UnusedButton,
|
_ => PointerRoute::UnusedButton,
|
||||||
}),
|
}),
|
||||||
WindowEvent::MouseWheel { delta, .. } => Some(PointerRoute::Wheel(*delta)),
|
WindowEvent::MouseWheel { delta, .. } => Some(PointerRoute::Wheel(*delta)),
|
||||||
|
|
@ -4610,7 +4636,6 @@ mod input_routing_tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn a_button_without_semantics_is_claimed_and_dropped() {
|
fn a_button_without_semantics_is_claimed_and_dropped() {
|
||||||
for button in [
|
for button in [
|
||||||
MouseButton::Middle,
|
|
||||||
MouseButton::Back,
|
MouseButton::Back,
|
||||||
MouseButton::Forward,
|
MouseButton::Forward,
|
||||||
MouseButton::Other(9),
|
MouseButton::Other(9),
|
||||||
|
|
@ -4624,6 +4649,14 @@ mod input_routing_tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// **The middle button is no longer semantics-free**: GUI Stage
|
||||||
|
// 1b's B4 gives its PRESS a meaning. Its RELEASE still has none,
|
||||||
|
// so it stays in this row rather than leaving it.
|
||||||
|
assert_eq!(
|
||||||
|
route_one(&mouse_input(ElementState::Released, MouseButton::Middle)),
|
||||||
|
Route::Pointer(PointerRoute::UnusedButton),
|
||||||
|
"a middle release remains nothing, like the right button's"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// P1 — the wheel delta is carried **raw**. Converting it to lines
|
/// P1 — the wheel delta is carried **raw**. Converting it to lines
|
||||||
|
|
@ -4939,7 +4972,10 @@ mod input_routing_tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn an_unused_button_produces_no_effect_of_any_kind() {
|
fn an_unused_button_produces_no_effect_of_any_kind() {
|
||||||
let mut h = EffectHarness::new();
|
let mut h = EffectHarness::new();
|
||||||
let step = h.feed(&mouse_input(ElementState::Pressed, MouseButton::Middle));
|
// `Back`, not `Middle`: B4 gave the middle PRESS a meaning, so
|
||||||
|
// this row moved to a button that still has none rather than
|
||||||
|
// being weakened to accommodate the new one.
|
||||||
|
let step = h.feed(&mouse_input(ElementState::Pressed, MouseButton::Back));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
step,
|
step,
|
||||||
Step {
|
Step {
|
||||||
|
|
@ -5076,7 +5112,10 @@ mod input_routing_tests {
|
||||||
position: PhysicalPosition::new(4.0, 8.0),
|
position: PhysicalPosition::new(4.0, 8.0),
|
||||||
},
|
},
|
||||||
mouse_input(ElementState::Pressed, MouseButton::Left),
|
mouse_input(ElementState::Pressed, MouseButton::Left),
|
||||||
mouse_input(ElementState::Pressed, MouseButton::Middle),
|
// `Back`: this row is about ORDER, and it keeps a
|
||||||
|
// semantics-free button so B4's new middle-press meaning
|
||||||
|
// does not quietly become part of what it asserts.
|
||||||
|
mouse_input(ElementState::Pressed, MouseButton::Back),
|
||||||
WindowEvent::RedrawRequested,
|
WindowEvent::RedrawRequested,
|
||||||
WindowEvent::Occluded(false),
|
WindowEvent::Occluded(false),
|
||||||
WindowEvent::CloseRequested,
|
WindowEvent::CloseRequested,
|
||||||
|
|
@ -5288,6 +5327,33 @@ const WHEEL_LINES_PER_TICK: f32 = 3.0;
|
||||||
/// tick", the horizontal twin of [`WHEEL_LINES_PER_TICK`].
|
/// tick", the horizontal twin of [`WHEEL_LINES_PER_TICK`].
|
||||||
const WHEEL_COLUMNS_PER_TICK: f32 = 3.0;
|
const WHEEL_COLUMNS_PER_TICK: f32 = 3.0;
|
||||||
|
|
||||||
|
/// Which OS selection a paste gesture reads.
|
||||||
|
///
|
||||||
|
/// X11 and Wayland carry two: the CLIPBOARD, written by an explicit
|
||||||
|
/// copy, and the PRIMARY selection, written merely by selecting text.
|
||||||
|
/// **They are different selections with different contents**, and the
|
||||||
|
/// platform convention pairs them with different gestures.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum PasteSource {
|
||||||
|
/// What Ctrl-V reads.
|
||||||
|
Clipboard,
|
||||||
|
/// What a middle click reads on Linux (GUI Stage 1b B4).
|
||||||
|
Primary,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The selection a middle click pastes from.
|
||||||
|
///
|
||||||
|
/// **PRIMARY on Linux** — B4's whole content. Elsewhere there is no
|
||||||
|
/// PRIMARY selection, so the gesture falls back to the clipboard rather
|
||||||
|
/// than doing nothing, which is the closest available meaning.
|
||||||
|
const fn middle_click_paste_source() -> PasteSource {
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
PasteSource::Primary
|
||||||
|
} else {
|
||||||
|
PasteSource::Clipboard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The wire scroll kinds for a banked `(x, y)` tick count, in order.
|
/// The wire scroll kinds for a banked `(x, y)` tick count, in order.
|
||||||
///
|
///
|
||||||
/// One event per whole tick: a single wheel notch that banks two ticks
|
/// One event per whole tick: a single wheel notch that banks two ticks
|
||||||
|
|
@ -6375,10 +6441,36 @@ impl State {
|
||||||
/// Read the OS clipboard as bytes (for Ctrl-V → `Paste`). `None` on
|
/// Read the OS clipboard as bytes (for Ctrl-V → `Paste`). `None` on
|
||||||
/// any failure (empty / non-text / unavailable).
|
/// any failure (empty / non-text / unavailable).
|
||||||
fn read_os_clipboard(&mut self) -> Option<Vec<u8>> {
|
fn read_os_clipboard(&mut self) -> Option<Vec<u8>> {
|
||||||
match self.os_clipboard()?.get_text() {
|
self.read_os_selection(PasteSource::Clipboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read one named OS selection.
|
||||||
|
///
|
||||||
|
/// GUI Stage 1b B4 needs the **PRIMARY** selection, which on Linux
|
||||||
|
/// is a different selection from the clipboard with different
|
||||||
|
/// contents. Reading the clipboard for a middle click would paste
|
||||||
|
/// whatever was last explicitly copied instead of what is currently
|
||||||
|
/// selected — a plausible-looking wrong answer, which is why B4's
|
||||||
|
/// row asserts the source rather than that "a paste happened".
|
||||||
|
fn read_os_selection(&mut self, source: PasteSource) -> Option<Vec<u8>> {
|
||||||
|
let clipboard = self.os_clipboard()?;
|
||||||
|
let read = match source {
|
||||||
|
PasteSource::Clipboard => clipboard.get_text(),
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
PasteSource::Primary => {
|
||||||
|
use arboard::{GetExtLinux, LinuxClipboardKind};
|
||||||
|
clipboard
|
||||||
|
.get()
|
||||||
|
.clipboard(LinuxClipboardKind::Primary)
|
||||||
|
.text()
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
PasteSource::Primary => clipboard.get_text(),
|
||||||
|
};
|
||||||
|
match read {
|
||||||
Ok(s) => Some(s.into_bytes()),
|
Ok(s) => Some(s.into_bytes()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("pmacs-gpu: clipboard read failed: {e}");
|
eprintln!("pmacs-gpu: {source:?} read failed: {e}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14610,6 +14702,65 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// B4 — a middle-click paste reads the **PRIMARY selection** on
|
||||||
|
/// Linux, not the clipboard.
|
||||||
|
///
|
||||||
|
/// The two are different selections with different contents: the
|
||||||
|
/// clipboard holds what was last explicitly copied, PRIMARY holds
|
||||||
|
/// what is currently selected. Reading the wrong one produces a
|
||||||
|
/// paste — just not the one the platform convention promises — so
|
||||||
|
/// the row asserts the SOURCE rather than that a paste happened.
|
||||||
|
///
|
||||||
|
/// *Mutation: return `PasteSource::Clipboard` → this row.*
|
||||||
|
#[test]
|
||||||
|
fn b4_a_middle_click_pastes_the_primary_selection_on_linux() {
|
||||||
|
let source = super::middle_click_paste_source();
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
assert_eq!(
|
||||||
|
source,
|
||||||
|
super::PasteSource::Primary,
|
||||||
|
"B4: the middle button reads PRIMARY on Linux"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert_eq!(
|
||||||
|
source,
|
||||||
|
super::PasteSource::Clipboard,
|
||||||
|
"off Linux there is no PRIMARY selection; the clipboard is \
|
||||||
|
the closest available meaning"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// B4's routing half — a middle **press** is its own route now, and
|
||||||
|
/// no longer the claimed-and-dropped `UnusedButton`.
|
||||||
|
///
|
||||||
|
/// Its RELEASE stays unused, like the right button's: the paste
|
||||||
|
/// happens once, on the press.
|
||||||
|
#[test]
|
||||||
|
fn b4_a_middle_press_routes_to_its_own_variant_and_its_release_does_not() {
|
||||||
|
use winit::event::{ElementState, MouseButton};
|
||||||
|
let press = winit::event::WindowEvent::MouseInput {
|
||||||
|
device_id: winit::event::DeviceId::dummy(),
|
||||||
|
state: ElementState::Pressed,
|
||||||
|
button: MouseButton::Middle,
|
||||||
|
};
|
||||||
|
let release = winit::event::WindowEvent::MouseInput {
|
||||||
|
device_id: winit::event::DeviceId::dummy(),
|
||||||
|
state: ElementState::Released,
|
||||||
|
button: MouseButton::Middle,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
super::route_pointer(&press),
|
||||||
|
Some(super::PointerRoute::MiddlePress),
|
||||||
|
"a middle press is B4's gesture"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::route_pointer(&release),
|
||||||
|
Some(super::PointerRoute::UnusedButton),
|
||||||
|
"and its release remains nothing, like the right button's"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// R4 and R5's resets exist and are SEPARATE, so a mutation that
|
/// R4 and R5's resets exist and are SEPARATE, so a mutation that
|
||||||
/// omits one is individually visible.
|
/// omits one is individually visible.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue