Skip to main content

brep_app/automation/
pointer.rs

1//! The stateful virtual pointer and keyboard (§6): position, held buttons and
2//! modifiers persist across frames; each input command becomes one or more
3//! `egui::Event`s pushed into the frame's `RawInput`.
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
7#[serde(rename_all = "snake_case")]
8pub enum Button {
9    Primary,
10    Secondary,
11    Middle,
12    Extra1,
13    Extra2,
14}
15
16impl Button {
17    pub fn egui(self) -> egui::PointerButton {
18        match self {
19            Button::Primary => egui::PointerButton::Primary,
20            Button::Secondary => egui::PointerButton::Secondary,
21            Button::Middle => egui::PointerButton::Middle,
22            Button::Extra1 => egui::PointerButton::Extra1,
23            Button::Extra2 => egui::PointerButton::Extra2,
24        }
25    }
26    fn index(self) -> usize {
27        self as usize
28    }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
32#[serde(rename_all = "snake_case")]
33pub enum WheelUnit {
34    Point,
35    Line,
36    Page,
37}
38
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
40#[serde(deny_unknown_fields)]
41pub struct Modifiers {
42    #[serde(default)]
43    pub ctrl: bool,
44    #[serde(default)]
45    pub shift: bool,
46    #[serde(default)]
47    pub alt: bool,
48    /// Cmd on macOS; treated as Ctrl elsewhere.
49    #[serde(default)]
50    pub command: bool,
51}
52
53impl Modifiers {
54    pub fn egui(self) -> egui::Modifiers {
55        egui::Modifiers {
56            alt: self.alt,
57            ctrl: self.ctrl || self.command,
58            shift: self.shift,
59            mac_cmd: self.command,
60            command: self.ctrl || self.command,
61        }
62    }
63}
64
65/// The pointer as reported to callers.
66#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
67pub struct PointerState {
68    /// Position in egui points, `null` when the pointer is not over the surface.
69    pub pos: Option<[f32; 2]>,
70    pub buttons: Vec<Button>,
71    pub modifiers: Modifiers,
72}
73
74/// egui's multi-click window: a release less than this after the last click,
75/// within `max_click_dist` of it, is a double click, and one less than TWICE
76/// this after the click before that is a triple (`InputOptions` default,
77/// `egui::input_state::PointerState::begin_pass`). The app keeps the default;
78/// `the_multi_click_window_is_egui_s` pins the copy.
79pub const MULTI_CLICK_WINDOW: f64 = 0.3;
80
81/// egui's click limits (`InputOptions` defaults): a release is a click only
82/// if the pointer stayed within this many points of the press ...
83pub const CLICK_DIST: f32 = 6.0;
84/// ... and the button was down no longer than this.
85pub const CLICK_DURATION: f64 = 0.8;
86
87#[derive(Debug, Clone, Default)]
88pub struct Pointer {
89    pub pos: Option<egui::Pos2>,
90    pub buttons: [bool; 5],
91    pub modifiers: Modifiers,
92    /// egui's clock for the frame being drained, set by
93    /// [`drain_input`](crate::automation::queue::AutomationQueue::drain_input)
94    /// before the frame's commands run.
95    pub now: f64,
96    /// Set by [`click_gap`](Self::click_gap): the queue replaces egui's
97    /// `PointerState` with a fresh one after this frame's commands.
98    pub reset_clicks: bool,
99    /// egui time of the last injected release that egui counts as a CLICK.
100    /// A drag's release is not one, and must not make the next click wait.
101    released: Option<f64>,
102    /// Where and when the held button went down, and whether the pointer has
103    /// since left [`CLICK_DIST`] of it: egui's own test for a click.
104    press: Option<(egui::Pos2, f64)>,
105    moved_far: bool,
106}
107
108const ALL_BUTTONS: [Button; 5] = [Button::Primary, Button::Secondary, Button::Middle, Button::Extra1, Button::Extra2];
109
110impl Pointer {
111    pub fn state(&self) -> PointerState {
112        PointerState {
113            pos: self.pos.map(|p| [p.x, p.y]),
114            buttons: ALL_BUTTONS.iter().copied().filter(|b| self.buttons[b.index()]).collect(),
115            modifiers: self.modifiers,
116        }
117    }
118
119    pub fn moved(&mut self, x: f32, y: f32, out: &mut Vec<egui::Event>) {
120        let pos = egui::pos2(x, y);
121        self.pos = Some(pos);
122        if let Some((origin, _)) = self.press {
123            self.moved_far |= origin.distance(pos) > CLICK_DIST;
124        }
125        out.push(egui::Event::PointerMoved(pos));
126    }
127
128    pub fn button(&mut self, button: Button, pressed: bool, out: &mut Vec<egui::Event>) -> Result<(), String> {
129        let pos = self.pos.ok_or("the pointer has no position yet: move it first")?;
130        self.buttons[button.index()] = pressed;
131        let now = self.now;
132        if pressed {
133            self.press = Some((pos, now));
134            self.moved_far = false;
135        } else if let Some((_, down)) = self.press.take() {
136            if !self.moved_far && now - down <= CLICK_DURATION {
137                self.released = Some(now);
138            }
139        }
140        out.push(egui::Event::PointerButton { pos, button: button.egui(), pressed, modifiers: self.modifiers.egui() });
141        Ok(())
142    }
143
144    /// Start a NEW click sequence: forget egui's click history when an
145    /// injected click is still inside its multi-click window. Returns whether
146    /// it did.
147    ///
148    /// egui counts clicks by time, and checks the place only against the LAST
149    /// click, so a click on one widget a few frames (1/60 s each headless)
150    /// before a double click on another makes the double a TRIPLE, and a
151    /// widget clicked twice by two scripted single clicks reads the second as
152    /// a double (ecad-kicad-dialog-folders-2026-09-23).
153    ///
154    /// The history is egui's `PointerState`, whose click times are private;
155    /// the queue swaps in `PointerState::default()` (its options are refreshed
156    /// every pass). egui's CLOCK is not touched: the first version jumped it
157    /// past the window instead, and the app's own timers saw the jump, so a
158    /// toast aged 0.6 s per click (ecad-mcp-click-count-export-wording).
159    /// The window is the TRIPLE one, twice the double's: the second click of
160    /// a new double looks two clicks back. Nothing is reset while a button is
161    /// held (a drag in progress lives in the same state), or when the last
162    /// click is already outside the window.
163    pub fn click_gap(&mut self) -> bool {
164        let recent = self.released.is_some_and(|t| self.now - t < 2.0 * MULTI_CLICK_WINDOW);
165        if !recent || self.buttons.iter().any(|&b| b) {
166            return false;
167        }
168        self.released = None;
169        self.reset_clicks = true;
170        true
171    }
172
173    pub fn gone(&mut self, out: &mut Vec<egui::Event>) {
174        self.pos = None;
175        self.buttons = [false; 5];
176        out.push(egui::Event::PointerGone);
177    }
178
179    pub fn wheel(&mut self, dx: f32, dy: f32, unit: WheelUnit, out: &mut Vec<egui::Event>) {
180        let unit = match unit {
181            WheelUnit::Point => egui::MouseWheelUnit::Point,
182            WheelUnit::Line => egui::MouseWheelUnit::Line,
183            WheelUnit::Page => egui::MouseWheelUnit::Page,
184        };
185        out.push(egui::Event::MouseWheel {
186            unit,
187            delta: egui::vec2(dx, dy),
188            phase: egui::TouchPhase::Move,
189            modifiers: self.modifiers.egui(),
190        });
191    }
192
193    pub fn set_modifiers(&mut self, m: Modifiers) {
194        self.modifiers = m;
195    }
196
197    pub fn key(&mut self, name: &str, pressed: bool, repeat: bool, out: &mut Vec<egui::Event>) -> Result<(), String> {
198        let key = egui::Key::from_name(name).ok_or_else(|| format!("unknown key `{name}` (egui key names: A..Z, 0..9, Enter, Escape, Tab, Space, Backspace, Delete, ArrowLeft/Right/Up/Down, Home, End, PageUp/Down, F1..F12, Minus, Plus, …)"))?;
199        out.push(egui::Event::Key { key, physical_key: None, pressed, repeat, modifiers: self.modifiers.egui() });
200        Ok(())
201    }
202
203    pub fn text(&mut self, text: &str, out: &mut Vec<egui::Event>) {
204        if !text.is_empty() {
205            out.push(egui::Event::Text(text.to_string()));
206        }
207    }
208}