macOS: Control-click opens the context menu, with its spelling suggestions (#1025)

This commit is contained in:
Marco Arnulfo
2026-10-14 15:31:35 -04:00
committed by GitHub
parent 8196624e83
commit 4fcf7da693
2 changed files with 75 additions and 1 deletions
+48
View File
@@ -3308,6 +3308,54 @@ mod tests {
assert_eq!(o.frame().unwrap().2.wrap, wordcraft_doc::para::Wrap::Square);
}
/// #996: on macOS a Control-click is the right-click (the trackpad's and the one-button
/// mouse's way to it), so it opens the same menu and leaves the caret where the menu needs it.
#[cfg(target_os = "macos")]
#[test]
fn control_click_on_macos_opens_the_context_menu() {
use egui_kittest::kittest::Queryable;
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());
a.run("text.insert", json!({"text": "The quick brwn fox"})).unwrap();
a.session.sel = wordcraft_engine::Selection::caret(Pos::body(0, 12));
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
for _ in 0..6 {
h.step();
}
let (p, ht) = caret_screen(h.state_mut()).unwrap();
let at = p + egui::vec2(0.0, ht / 2.0);
h.state_mut().session.sel = wordcraft_engine::Selection::caret(Pos::body(0, 0));
h.step();
let ctrl = egui::Modifiers { ctrl: true, ..Default::default() };
// The events winit delivers for a Control-click, through the app's input hook as eframe runs it.
let send = |h: &mut egui_kittest::Harness<'_, WordApp>, events: Vec<egui::Event>| {
let mut raw = egui::RawInput { events, ..Default::default() };
h.state_mut().raw_input_hook(&mut raw);
for e in raw.events {
h.event(e);
}
h.step();
};
send(&mut h, vec![egui::Event::ModifiersChanged(ctrl), egui::Event::PointerMoved(at)]);
for pressed in [true, false] {
send(&mut h, vec![egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: ctrl }]);
}
h.event(egui::Event::ModifiersChanged(Default::default()));
h.step();
assert!(h.state().canvas.context_menu_open, "the Control-click opened the menu");
h.get_by_label("brown").click();
for _ in 0..3 {
h.step();
}
assert_eq!(h.state().session.doc.plain_text(StoryRef::Body), "The quick brown fox");
}
#[test]
fn only_web_and_email_links_are_followed() {
for ok in ["https://example.com/a?b#c", "http://example.com", "HTTPS://EXAMPLE.COM", "mailto:someone@example.com"] {
+27 -1
View File
@@ -442,6 +442,8 @@ pub struct WordApp {
queued_shots: Vec<(u64, f64, u32)>,
pending_shots: Vec<(u64, Option<String>, std::sync::mpsc::Sender<Value>, f64)>,
pub(crate) synthetic: Vec<egui::Event>,
/// macOS: the primary button went down with Control, so it is being read as the secondary.
control_click: bool,
/// Badge rects recorded during layout for the current keytip phase (cleared by `logic`, painted by `show`).
pub(crate) keytip_rects: Vec<(egui::Rect, String)>,
/// The Navigation pane's search box takes the keyboard on its next frame (Ctrl+F, #712).
@@ -571,6 +573,7 @@ impl WordApp {
queued_shots: Vec::new(),
pending_shots: Vec::new(),
synthetic: Vec::new(),
control_click: false,
keytip_rects: Vec::new(),
nav_search_focus: false,
styled: false,
@@ -1776,8 +1779,12 @@ impl WordApp {
}
}
/// Inject synthetic input events from the control channel (one pointer event per frame).
/// Read a Control-click as the right-click on macOS, then inject synthetic input events from
/// the control channel (one pointer event per frame).
pub fn raw_input_hook(&mut self, raw: &mut egui::RawInput) {
if cfg!(target_os = "macos") {
control_click_as_secondary(&mut raw.events, &mut self.control_click);
}
if self.synthetic.is_empty() {
return;
}
@@ -2227,6 +2234,25 @@ pub fn now_ms() -> f64 {
}
}
/// macOS reads a Control-click as the right-click (winit reports the primary button with Control
/// held), so menus open on it as on a two-finger click. The release follows its press even if
/// Control was let go in between.
fn control_click_as_secondary(events: &mut [egui::Event], held: &mut bool) {
for e in events {
if let egui::Event::PointerButton { button: button @ egui::PointerButton::Primary, pressed, modifiers, .. } = e {
if *pressed {
*held = modifiers.ctrl && !modifiers.mac_cmd;
}
if *held {
*button = egui::PointerButton::Secondary;
}
if !*pressed {
*held = false;
}
}
}
}
#[cfg(test)]
mod tests_perf;