agentmux_cef\client/
handlers.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Cef handler trait wrappers for AgentMuxHandler. Extracted from
5//! client/mod.rs in task #182 PR-G.
6//!
7//! Each block is a macro invocation that generates a small wrapper
8//! struct delegating to AgentMuxHandler methods.
9
10use std::sync::Arc;
11use cef::*;
12use parking_lot::Mutex;
13
14use super::AgentMuxHandler;
15
16// ---------------------------------------------------------------------------
17
18wrap_client! {
19    pub struct AgentMuxClient {
20        inner: Arc<Mutex<AgentMuxHandler>>,
21        is_browser_pane: bool,
22    }
23
24    impl Client {
25        fn display_handler(&self) -> Option<DisplayHandler> {
26            Some(AgentMuxDisplayHandler::new(self.inner.clone()))
27        }
28
29        fn keyboard_handler(&self) -> Option<KeyboardHandler> {
30            Some(AgentMuxKeyboardHandler::new())
31        }
32
33        fn life_span_handler(&self) -> Option<LifeSpanHandler> {
34            Some(AgentMuxLifeSpanHandler::new(self.inner.clone()))
35        }
36
37        fn load_handler(&self) -> Option<LoadHandler> {
38            Some(AgentMuxLoadHandler::new(self.inner.clone()))
39        }
40
41        fn request_handler(&self) -> Option<RequestHandler> {
42            Some(AgentMuxRequestHandler::new(self.inner.clone()))
43        }
44
45        fn drag_handler(&self) -> Option<DragHandler> {
46            if self.is_browser_pane {
47                return None;
48            }
49            Some(AgentMuxDragHandler::new(self.inner.clone()))
50        }
51
52        fn focus_handler(&self) -> Option<FocusHandler> {
53            // For browser panes only: cancel CEF's auto-focus on navigation so the
54            // child HWND doesn't steal keyboard focus from the main window when the
55            // page finishes loading. The user can still click into the pane to focus it.
56            if self.is_browser_pane {
57                Some(AgentMuxPaneFocusHandler::new())
58            } else {
59                None
60            }
61        }
62    }
63}
64
65// FocusHandler used only by browser-pane clients. Returns 0 for every
66// focus source (never cancels at the CEF level) — cancelling NAVIGATION
67// focus during the very first navigation of a newly-created pane fires
68// CEF's `on_before_close` on that pane ~10ms later. Focus-steal
69// protection lives entirely in the Win32 `WndProc` subclass below
70// (`browser_pane::hwnd::install_browser_pane_focus_redirect`), which redirects programmatic
71// `WM_SETFOCUS` back to the top-level window. User clicks are let through
72// because `WM_LBUTTONDOWN` in the subclass arms `ALLOW_BROWSER_PANE_FOCUS_ONCE`.
73wrap_focus_handler! {
74    struct AgentMuxPaneFocusHandler;
75
76    impl FocusHandler {
77        fn on_set_focus(
78            &self,
79            _browser: Option<&mut Browser>,
80            source: FocusSource,
81        ) -> ::std::os::raw::c_int {
82            // Previously we cancelled FocusSource::NAVIGATION here to
83            // stop page-load from stealing focus away from the main
84            // window. But cancelling on_set_focus during the very
85            // first navigation of a newly-created pane triggered CEF
86            // to fire `on_before_close` on that pane ~10ms later —
87            // reliably reproducible when creating a 2nd browser pane.
88            // The Win32 WndProc subclass below already redirects
89            // page-load SetFocus to the top-level window (see
90            // `browser_pane::hwnd::install_browser_pane_focus_redirect`), which
91            // handles the original focus-steal concern. Returning 0
92            // here so CEF proceeds with normal focus handling at the
93            // Chromium level; Win32 subclass continues to redirect
94            // any resulting Win32 focus change away from the pane.
95            tracing::info!("[pane-focus] on_set_focus source={:?} cancel=false", source);
96            0
97        }
98    }
99}
100
101// ---------------------------------------------------------------------------
102// DragHandler — handles `-webkit-app-region: drag` regions reported by the
103// renderer (used on macOS/Windows where native draggable regions work).
104//
105// NOTE(Linux): On Linux/Wayland we do NOT use -webkit-app-region: drag for
106// window-move because Chromium suppresses ALL events on drag regions before
107// they reach the renderer (verified empirically), making drag mutually
108// exclusive with right-click contextmenu on the same element. Linux drag is
109// JS-driven instead — see frontend/app/hook/useWindowDrag.linux.ts and
110// the start_window_drag IPC → CefWindow::BeginWindowDrag() (CEF source
111// patch in agentmux/7680-... branch). Retro:
112// docs/retros/2026-05-02-drag-and-rightclick-coexistence.md.
113
114wrap_drag_handler! {
115    struct AgentMuxDragHandler {
116        inner: Arc<Mutex<AgentMuxHandler>>,
117    }
118
119    impl DragHandler {
120        fn on_draggable_regions_changed(
121            &self,
122            browser: Option<&mut Browser>,
123            _frame: Option<&mut Frame>,
124            regions: Option<&[DraggableRegion]>,
125        ) {
126            if let Some(rs) = regions {
127                let summary: Vec<String> = rs.iter().map(|r| {
128                    format!("{}x{}@{},{} drag={}", r.bounds.width, r.bounds.height, r.bounds.x, r.bounds.y, r.draggable != 0)
129                }).collect();
130                tracing::info!("[drag_handler] on_draggable_regions_changed: {} regions — {:?}", rs.len(), summary);
131            } else {
132                tracing::info!("[drag_handler] on_draggable_regions_changed: None");
133            }
134            let mut browser = browser.cloned();
135            let Some(browser_view) = browser_view_get_for_browser(browser.as_mut()) else { return };
136            let Some(window) = browser_view.window() else { return };
137            window.set_draggable_regions(regions);
138        }
139    }
140}
141
142// KeyboardHandler — intercept Ctrl+<key> shortcuts before CEF/Chromium
143// consumes them (e.g., Ctrl+P = print, Ctrl+G = find-next).
144// Returning true from on_pre_key_event tells CEF "handled" so it won't
145// trigger the built-in action; the key still reaches JavaScript.
146// ---------------------------------------------------------------------------
147
148/// CEF event flag: Ctrl key is held.
149const EVENTFLAG_CONTROL_DOWN: u32 = 1 << 2;
150
151/// Windows virtual-key codes for shortcuts we want to forward to JS.
152const VK_P: i32 = 0x50; // Ctrl+P — command palette (not print)
153const VK_G: i32 = 0x47; // Ctrl+G — (reserve for app use)
154
155wrap_keyboard_handler! {
156    struct AgentMuxKeyboardHandler;
157
158    impl KeyboardHandler {
159        fn on_pre_key_event(
160            &self,
161            _browser: Option<&mut Browser>,
162            event: Option<&KeyEvent>,
163            _os_event: Option<&mut crate::OsKeyEvent>,
164            is_keyboard_shortcut: Option<&mut ::std::os::raw::c_int>,
165        ) -> ::std::os::raw::c_int {
166            if let Some(ev) = event {
167                let ctrl = (ev.modifiers & EVENTFLAG_CONTROL_DOWN) != 0;
168                if ctrl && matches!(ev.windows_key_code, VK_P | VK_G) {
169                    // Tell CEF this is a keyboard shortcut so it dispatches
170                    // the keydown event to JavaScript instead of handling it
171                    // as a built-in browser action (print dialog, etc.).
172                    if let Some(flag) = is_keyboard_shortcut {
173                        *flag = 1;
174                    }
175                    // Return 0 = not consumed at pre-key stage; CEF will
176                    // still call on_key_event where we return 0 again,
177                    // letting JS handle it via the normal keydown path.
178                }
179            }
180            0 // not consumed
181        }
182    }
183}
184
185// ---------------------------------------------------------------------------
186// DisplayHandler — title changes
187// ---------------------------------------------------------------------------
188
189wrap_display_handler! {
190    struct AgentMuxDisplayHandler {
191        inner: Arc<Mutex<AgentMuxHandler>>,
192    }
193
194    impl DisplayHandler {
195        fn on_title_change(&self, browser: Option<&mut Browser>, title: Option<&CefString>) {
196            let mut inner = self.inner.lock();
197            inner.on_title_change(browser, title);
198        }
199
200        fn on_favicon_urlchange(
201            &self,
202            browser: Option<&mut Browser>,
203            icon_urls: Option<&mut CefStringList>,
204        ) {
205            let mut inner = self.inner.lock();
206            inner.on_favicon_urlchange(browser, icon_urls);
207        }
208    }
209}
210
211// ---------------------------------------------------------------------------
212// LifeSpanHandler — browser creation/destruction
213// ---------------------------------------------------------------------------
214
215wrap_life_span_handler! {
216    struct AgentMuxLifeSpanHandler {
217        inner: Arc<Mutex<AgentMuxHandler>>,
218    }
219
220    impl LifeSpanHandler {
221        fn on_after_created(&self, browser: Option<&mut Browser>) {
222            let mut inner = self.inner.lock();
223            inner.on_after_created(browser);
224        }
225
226        fn do_close(&self, browser: Option<&mut Browser>) -> i32 {
227            let mut inner = self.inner.lock();
228            inner.do_close(browser).into()
229        }
230
231        fn on_before_close(&self, browser: Option<&mut Browser>) {
232            let mut inner = self.inner.lock();
233            inner.on_before_close(browser);
234        }
235
236        fn on_before_popup(
237            &self,
238            browser: Option<&mut Browser>,
239            frame: Option<&mut Frame>,
240            _popup_id: ::std::os::raw::c_int,
241            target_url: Option<&CefString>,
242            _target_frame_name: Option<&CefString>,
243            target_disposition: WindowOpenDisposition,
244            _user_gesture: ::std::os::raw::c_int,
245            _popup_features: Option<&PopupFeatures>,
246            _window_info: Option<&mut WindowInfo>,
247            _client: Option<&mut Option<Client>>,
248            _settings: Option<&mut BrowserSettings>,
249            _extra_info: Option<&mut Option<DictionaryValue>>,
250            _no_javascript_access: Option<&mut ::std::os::raw::c_int>,
251        ) -> ::std::os::raw::c_int {
252            let mut inner = self.inner.lock();
253            if inner.on_before_popup(browser, frame, target_url, target_disposition) {
254                1
255            } else {
256                0
257            }
258        }
259    }
260}
261
262// ---------------------------------------------------------------------------
263// LoadHandler — load events and errors
264// ---------------------------------------------------------------------------
265
266wrap_load_handler! {
267    struct AgentMuxLoadHandler {
268        inner: Arc<Mutex<AgentMuxHandler>>,
269    }
270
271    impl LoadHandler {
272        fn on_loading_state_change(
273            &self,
274            browser: Option<&mut Browser>,
275            is_loading: ::std::os::raw::c_int,
276            can_go_back: ::std::os::raw::c_int,
277            can_go_forward: ::std::os::raw::c_int,
278        ) {
279            let mut inner = self.inner.lock();
280            inner.on_loading_state_change(browser, is_loading, can_go_back, can_go_forward);
281        }
282
283        fn on_load_end(
284            &self,
285            browser: Option<&mut Browser>,
286            frame: Option<&mut Frame>,
287            http_status_code: i32,
288        ) {
289            let mut inner = self.inner.lock();
290            inner.on_load_end(browser, frame, http_status_code);
291        }
292
293        fn on_load_error(
294            &self,
295            browser: Option<&mut Browser>,
296            frame: Option<&mut Frame>,
297            error_code: Errorcode,
298            error_text: Option<&CefString>,
299            failed_url: Option<&CefString>,
300        ) {
301            let mut inner = self.inner.lock();
302            inner.on_load_error(browser, frame, error_code, error_text, failed_url);
303        }
304    }
305}
306
307// ---------------------------------------------------------------------------
308// RequestHandler — render-process termination (white-screen recovery)
309// ---------------------------------------------------------------------------
310//
311// We only override `on_render_process_terminated` here. Everything else
312// inherits the default (no-op) implementations from the cef-rs trait.
313// See SPEC_GRACEFUL_CRASH_HANDLING_2026_04_13.md (PR 1).
314
315wrap_request_handler! {
316    struct AgentMuxRequestHandler {
317        inner: Arc<Mutex<AgentMuxHandler>>,
318    }
319
320    impl RequestHandler {
321        fn on_render_process_terminated(
322            &self,
323            browser: Option<&mut Browser>,
324            status: TerminationStatus,
325            error_code: ::std::os::raw::c_int,
326            error_string: Option<&CefString>,
327        ) {
328            let mut inner = self.inner.lock();
329            inner.on_render_process_terminated(browser, status, error_code, error_string);
330        }
331
332        // HTTP Basic / Digest auth challenge. Phase α of
333        // SPEC_BROWSER_PANE_HTTP_BASIC_AUTH_2026_05_18.md. Returns 1
334        // (async) so CEF holds the request open while we surface the
335        // credential prompt to the user.
336        fn auth_credentials(
337            &self,
338            browser: Option<&mut Browser>,
339            origin_url: Option<&CefString>,
340            is_proxy: ::std::os::raw::c_int,
341            host: Option<&CefString>,
342            port: ::std::os::raw::c_int,
343            realm: Option<&CefString>,
344            scheme: Option<&CefString>,
345            callback: Option<&mut AuthCallback>,
346        ) -> ::std::os::raw::c_int {
347            let mut inner = self.inner.lock();
348            inner.on_auth_credentials(
349                browser, origin_url, is_proxy, host, port, realm, scheme, callback,
350            )
351        }
352    }
353}