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}