Skip to main content

cbf_chrome/ffi/
client.rs

1use std::{
2    ffi::{CStr, CString},
3    ptr,
4    time::Duration,
5};
6
7use cbf::data::{edit::EditAction, window_open::WindowOpenResponse};
8use cbf_chrome_sys::ffi::*;
9use tracing::warn;
10
11use super::map::{
12    ime_range_to_ffi, key_event_type_to_ffi, mouse_button_to_ffi, mouse_event_type_to_ffi,
13    parse_event, parse_extension_list, pointer_type_to_ffi, scroll_granularity_to_ffi,
14    to_ffi_ime_text_spans,
15};
16use super::utils::{c_string_to_string, to_optional_cstring};
17use super::{Error, IpcEvent};
18use crate::data::{
19    browsing_context_open::ChromeBrowsingContextOpenResponse,
20    download::ChromeDownloadId,
21    drag::{ChromeDragDrop, ChromeDragUpdate},
22    extension::ChromeExtensionInfo,
23    ids::{PopupId, TabId},
24    ime::{
25        ChromeConfirmCompositionBehavior, ChromeImeCommitText, ChromeImeComposition,
26        ChromeTransientImeCommitText, ChromeTransientImeComposition,
27    },
28    input::{ChromeKeyEvent, ChromeMouseWheelEvent},
29    mouse::ChromeMouseEvent,
30    profile::ChromeProfileInfo,
31    prompt_ui::{PromptUiId, PromptUiResponse},
32};
33
34/// Client wrapper for the CBF IPC bridge.
35pub struct IpcClient {
36    inner: *mut CbfBridgeClientHandle,
37}
38
39#[derive(Debug, Clone, Copy)]
40pub(crate) struct IpcEventWaitHandle {
41    inner: *mut CbfBridgeClientHandle,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum EventWaitResult {
46    EventAvailable,
47    TimedOut,
48    Disconnected,
49    Closed,
50}
51
52// SAFETY: IpcClient owns the bridge client handle and its methods
53// serialize access through the Mojo thread internally. The handle
54// is not shared, only moved across the process::start_chromium →
55// backend thread boundary exactly once.
56unsafe impl Send for IpcClient {}
57// SAFETY: `cbf_bridge_client_wait_for_event` synchronizes through the bridge's
58// internal event wait state. This handle is non-owning and only used while the
59// owning `IpcClient` remains alive.
60unsafe impl Send for IpcEventWaitHandle {}
61// SAFETY: The bridge wait path is internally synchronized; this wrapper only
62// exposes `wait_for_event` and does not own the underlying handle.
63unsafe impl Sync for IpcEventWaitHandle {}
64
65impl std::fmt::Debug for IpcClient {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.debug_struct("IpcClient")
68            .field("inner", &format!("{:p}", self.inner))
69            .finish()
70    }
71}
72
73impl IpcClient {
74    /// Prepare the Mojo channel before spawning the Chromium process.
75    ///
76    /// Returns `(remote_fd, switch_arg)` where:
77    /// - `remote_fd` is the file descriptor of the remote channel endpoint that
78    ///   must be inherited by the child process (Unix only; -1 on other platforms).
79    /// - `switch_arg` is the command-line switch that Chromium needs to recover
80    ///   the endpoint (e.g. `--cbf-ipc-handle=...`).
81    pub fn prepare_channel() -> Result<(i32, String), Error> {
82        let mut buf = [0u8; 512];
83        let fd = unsafe {
84            cbf_bridge_init();
85            cbf_bridge_prepare_channel(
86                buf.as_mut_ptr() as *mut std::os::raw::c_char,
87                buf.len() as i32,
88            )
89        };
90        let switch_arg = CStr::from_bytes_until_nul(&buf)
91            .map_err(|_| Error::ConnectionFailed)?
92            .to_str()
93            .map_err(|_| Error::ConnectionFailed)?
94            .to_owned();
95        Ok((fd, switch_arg))
96    }
97
98    /// Notify the bridge of the spawned child's PID.
99    ///
100    /// Must be called after spawning the Chromium process and before
101    /// `connect_inherited`. On macOS this registers the Mach port with the
102    /// rendezvous server; on other platforms it completes channel bookkeeping.
103    pub fn pass_child_pid(pid: u32) {
104        unsafe { cbf_bridge_pass_child_pid(pid as i64) }
105    }
106
107    /// Wrap a pre-created bridge client handle and complete the Mojo connection.
108    ///
109    /// `inner` must have been created by `cbf_bridge_client_create()` and the
110    /// channel must have been prepared with `prepare_channel()` before calling
111    /// this function (after the child process has been spawned).
112    ///
113    /// # Safety
114    ///
115    /// `inner` must be a valid, live `CbfBridgeClientHandle` allocated by
116    /// `cbf_bridge_client_create()`. Ownership is transferred to the returned
117    /// `IpcClient` on success and consumed by this function on failure.
118    pub unsafe fn connect_inherited(inner: *mut CbfBridgeClientHandle) -> Result<Self, Error> {
119        if inner.is_null() {
120            return Err(Error::ConnectionFailed);
121        }
122        let connected = unsafe { cbf_bridge_client_connect_inherited(inner) };
123        if !connected {
124            warn!(
125                result = "err",
126                error = "ipc_connect_inherited_failed",
127                "IPC inherited connect failed"
128            );
129            unsafe { cbf_bridge_client_destroy(inner) };
130            return Err(Error::ConnectionFailed);
131        }
132        Ok(Self { inner })
133    }
134
135    /// Authenticate with the session token and set up the browser observer.
136    ///
137    /// Must be called once after `connect_inherited` and before any other method.
138    pub fn authenticate(&self, token: &str) -> Result<(), Error> {
139        if self.inner.is_null() {
140            return Err(Error::ConnectionFailed);
141        }
142        let token = CString::new(token).map_err(|_| Error::InvalidInput)?;
143        if unsafe { cbf_bridge_client_authenticate(self.inner, token.as_ptr()) } {
144            Ok(())
145        } else {
146            Err(Error::ConnectionFailed)
147        }
148    }
149
150    /// Wait until an event is available or the bridge closes.
151    pub fn wait_for_event(&self, timeout: Option<Duration>) -> Result<EventWaitResult, Error> {
152        wait_for_event_inner(self.inner, timeout)
153    }
154
155    pub(crate) fn event_wait_handle(&self) -> IpcEventWaitHandle {
156        IpcEventWaitHandle { inner: self.inner }
157    }
158
159    /// Poll the next IPC event, if any, from the backend.
160    pub fn poll_event(&mut self) -> Option<Result<IpcEvent, Error>> {
161        if self.inner.is_null() {
162            return None;
163        }
164
165        let mut event = CbfBridgeEvent::default();
166        if !unsafe { cbf_bridge_client_poll_event(self.inner, &mut event) } {
167            return None;
168        }
169
170        let parsed = parse_event(event);
171        unsafe { cbf_bridge_event_free(&mut event) };
172
173        if let Err(err) = &parsed {
174            warn!(
175                result = "err",
176                error = "ipc_event_parse_failed",
177                err = ?err,
178                "IPC event parse failed"
179            );
180        }
181
182        Some(parsed)
183    }
184
185    /// Retrieve the list of browser profiles from the backend.
186    pub fn list_profiles(&mut self) -> Result<Vec<ChromeProfileInfo>, Error> {
187        if self.inner.is_null() {
188            return Err(Error::ConnectionFailed);
189        }
190
191        let mut list = CbfProfileList::default();
192        if !unsafe { cbf_bridge_client_get_profiles(self.inner, &mut list) } {
193            return Err(Error::ConnectionFailed);
194        }
195
196        let profiles = if list.len == 0 || list.profiles.is_null() {
197            &[]
198        } else {
199            unsafe { std::slice::from_raw_parts(list.profiles, list.len as usize) }
200        };
201        let mut result = Vec::with_capacity(profiles.len());
202
203        for profile in profiles {
204            result.push(ChromeProfileInfo {
205                profile_id: c_string_to_string(profile.profile_id),
206                profile_path: c_string_to_string(profile.profile_path),
207                display_name: c_string_to_string(profile.display_name),
208                is_default: profile.is_default,
209            });
210        }
211
212        unsafe { cbf_bridge_profile_list_free(&mut list) };
213
214        Ok(result)
215    }
216
217    /// Retrieve the list of extensions from the backend.
218    pub fn list_extensions(&mut self, profile_id: &str) -> Result<Vec<ChromeExtensionInfo>, Error> {
219        if self.inner.is_null() {
220            return Err(Error::ConnectionFailed);
221        }
222
223        let profile = CString::new(profile_id).map_err(|_| Error::InvalidInput)?;
224
225        let mut list = CbfExtensionInfoList::default();
226        if !unsafe { cbf_bridge_client_list_extensions(self.inner, profile.as_ptr(), &mut list) } {
227            return Err(Error::ConnectionFailed);
228        }
229
230        let result = parse_extension_list(list);
231
232        unsafe { cbf_bridge_extension_list_free(&mut list) };
233        Ok(result)
234    }
235
236    pub fn activate_extension_action(
237        &mut self,
238        browsing_context_id: TabId,
239        extension_id: &str,
240    ) -> Result<(), Error> {
241        if self.inner.is_null() {
242            return Err(Error::ConnectionFailed);
243        }
244
245        let extension_id = CString::new(extension_id).map_err(|_| Error::InvalidInput)?;
246        if unsafe {
247            cbf_bridge_client_activate_extension_action(
248                self.inner,
249                browsing_context_id.get(),
250                extension_id.as_ptr(),
251            )
252        } {
253            Ok(())
254        } else {
255            Err(Error::ConnectionFailed)
256        }
257    }
258
259    /// Create a tab via the IPC bridge.
260    pub fn create_tab(
261        &mut self,
262        request_id: u64,
263        initial_url: &str,
264        profile_id: &str,
265    ) -> Result<(), Error> {
266        if self.inner.is_null() {
267            return Err(Error::ConnectionFailed);
268        }
269
270        let url = CString::new(initial_url).map_err(|_| Error::InvalidInput)?;
271        let profile = CString::new(profile_id).map_err(|_| Error::InvalidInput)?;
272
273        if unsafe {
274            cbf_bridge_client_create_tab(self.inner, request_id, url.as_ptr(), profile.as_ptr())
275        } {
276            Ok(())
277        } else {
278            Err(Error::ConnectionFailed)
279        }
280    }
281
282    /// Request closing the specified tab.
283    pub fn request_close_tab(&mut self, browsing_context_id: TabId) -> Result<(), Error> {
284        if self.inner.is_null() {
285            return Err(Error::ConnectionFailed);
286        }
287
288        if unsafe { cbf_bridge_client_request_close_tab(self.inner, browsing_context_id.get()) } {
289            Ok(())
290        } else {
291            Err(Error::ConnectionFailed)
292        }
293    }
294
295    /// Update the surface size of the specified tab.
296    pub fn set_tab_size(
297        &mut self,
298        browsing_context_id: TabId,
299        width: u32,
300        height: u32,
301    ) -> Result<(), Error> {
302        if self.inner.is_null() {
303            return Err(Error::ConnectionFailed);
304        }
305
306        if unsafe {
307            cbf_bridge_client_set_tab_size(self.inner, browsing_context_id.get(), width, height)
308        } {
309            Ok(())
310        } else {
311            Err(Error::ConnectionFailed)
312        }
313    }
314
315    /// Update whether the specified tab should receive text input focus.
316    pub fn set_tab_focus(
317        &mut self,
318        browsing_context_id: TabId,
319        focused: bool,
320    ) -> Result<(), Error> {
321        if self.inner.is_null() {
322            return Err(Error::ConnectionFailed);
323        }
324
325        if unsafe {
326            cbf_bridge_client_set_tab_focus(self.inner, browsing_context_id.get(), focused)
327        } {
328            Ok(())
329        } else {
330            Err(Error::ConnectionFailed)
331        }
332    }
333
334    /// Respond to a beforeunload confirmation request.
335    pub fn confirm_beforeunload(
336        &mut self,
337        browsing_context_id: TabId,
338        request_id: u64,
339        proceed: bool,
340    ) -> Result<(), Error> {
341        if self.inner.is_null() {
342            return Err(Error::ConnectionFailed);
343        }
344
345        if unsafe {
346            cbf_bridge_client_confirm_beforeunload(
347                self.inner,
348                browsing_context_id.get(),
349                request_id,
350                proceed,
351            )
352        } {
353            Ok(())
354        } else {
355            Err(Error::ConnectionFailed)
356        }
357    }
358
359    /// Respond to a JavaScript dialog request for a tab.
360    pub fn respond_javascript_dialog(
361        &mut self,
362        browsing_context_id: TabId,
363        request_id: u64,
364        accept: bool,
365        prompt_text: Option<&str>,
366    ) -> Result<(), Error> {
367        if self.inner.is_null() {
368            return Err(Error::ConnectionFailed);
369        }
370
371        let prompt_text = to_optional_cstring(&prompt_text.map(ToOwned::to_owned))
372            .map_err(|_| Error::InvalidInput)?;
373
374        if unsafe {
375            cbf_bridge_client_respond_javascript_dialog(
376                self.inner,
377                browsing_context_id.get(),
378                request_id,
379                accept,
380                prompt_text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
381            )
382        } {
383            Ok(())
384        } else {
385            Err(Error::ConnectionFailed)
386        }
387    }
388
389    /// Respond to a JavaScript dialog request for an extension popup.
390    pub fn respond_extension_popup_javascript_dialog(
391        &mut self,
392        popup_id: PopupId,
393        request_id: u64,
394        accept: bool,
395        prompt_text: Option<&str>,
396    ) -> Result<(), Error> {
397        if self.inner.is_null() {
398            return Err(Error::ConnectionFailed);
399        }
400
401        let prompt_text = to_optional_cstring(&prompt_text.map(ToOwned::to_owned))
402            .map_err(|_| Error::InvalidInput)?;
403
404        if unsafe {
405            cbf_bridge_client_respond_extension_popup_javascript_dialog(
406                self.inner,
407                popup_id.get(),
408                request_id,
409                accept,
410                prompt_text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
411            )
412        } {
413            Ok(())
414        } else {
415            Err(Error::ConnectionFailed)
416        }
417    }
418
419    /// Navigate the page to the provided URL.
420    pub fn navigate(&mut self, browsing_context_id: TabId, url: &str) -> Result<(), Error> {
421        if self.inner.is_null() {
422            return Err(Error::ConnectionFailed);
423        }
424
425        let url = CString::new(url).map_err(|_| Error::InvalidInput)?;
426
427        if unsafe {
428            cbf_bridge_client_navigate(self.inner, browsing_context_id.get(), url.as_ptr())
429        } {
430            Ok(())
431        } else {
432            Err(Error::ConnectionFailed)
433        }
434    }
435
436    /// Navigate back in history for the page.
437    pub fn go_back(&mut self, browsing_context_id: TabId) -> Result<(), Error> {
438        if self.inner.is_null() {
439            return Err(Error::ConnectionFailed);
440        }
441
442        if unsafe { cbf_bridge_client_go_back(self.inner, browsing_context_id.get()) } {
443            Ok(())
444        } else {
445            Err(Error::ConnectionFailed)
446        }
447    }
448
449    /// Navigate forward in history for the page.
450    pub fn go_forward(&mut self, browsing_context_id: TabId) -> Result<(), Error> {
451        if self.inner.is_null() {
452            return Err(Error::ConnectionFailed);
453        }
454
455        if unsafe { cbf_bridge_client_go_forward(self.inner, browsing_context_id.get()) } {
456            Ok(())
457        } else {
458            Err(Error::ConnectionFailed)
459        }
460    }
461
462    /// Reload the page, optionally ignoring caches.
463    pub fn reload(&mut self, browsing_context_id: TabId, ignore_cache: bool) -> Result<(), Error> {
464        if self.inner.is_null() {
465            return Err(Error::ConnectionFailed);
466        }
467
468        if unsafe { cbf_bridge_client_reload(self.inner, browsing_context_id.get(), ignore_cache) }
469        {
470            Ok(())
471        } else {
472            Err(Error::ConnectionFailed)
473        }
474    }
475
476    /// Open print preview for the page.
477    pub fn print_preview(&mut self, browsing_context_id: TabId) -> Result<(), Error> {
478        if self.inner.is_null() {
479            return Err(Error::ConnectionFailed);
480        }
481
482        if unsafe { cbf_bridge_client_print_preview(self.inner, browsing_context_id.get()) } {
483            Ok(())
484        } else {
485            Err(Error::ConnectionFailed)
486        }
487    }
488
489    /// Open DevTools for the specified page.
490    pub fn open_dev_tools(&mut self, browsing_context_id: TabId) -> Result<(), Error> {
491        if self.inner.is_null() {
492            return Err(Error::ConnectionFailed);
493        }
494
495        if unsafe { cbf_bridge_client_open_dev_tools(self.inner, browsing_context_id.get()) } {
496            Ok(())
497        } else {
498            Err(Error::ConnectionFailed)
499        }
500    }
501
502    /// Open DevTools and inspect the element at the given coordinates.
503    pub fn inspect_element(
504        &mut self,
505        browsing_context_id: TabId,
506        x: i32,
507        y: i32,
508    ) -> Result<(), Error> {
509        if self.inner.is_null() {
510            return Err(Error::ConnectionFailed);
511        }
512
513        if unsafe { cbf_bridge_client_inspect_element(self.inner, browsing_context_id.get(), x, y) }
514        {
515            Ok(())
516        } else {
517            Err(Error::ConnectionFailed)
518        }
519    }
520
521    /// Request the DOM HTML for the specified page.
522    pub fn get_tab_dom_html(
523        &mut self,
524        browsing_context_id: TabId,
525        request_id: u64,
526    ) -> Result<(), Error> {
527        if self.inner.is_null() {
528            return Err(Error::ConnectionFailed);
529        }
530
531        if unsafe {
532            cbf_bridge_client_get_tab_dom_html(self.inner, browsing_context_id.get(), request_id)
533        } {
534            Ok(())
535        } else {
536            Err(Error::ConnectionFailed)
537        }
538    }
539
540    /// Open Chromium default PromptUi for pending request.
541    pub fn open_default_prompt_ui(
542        &mut self,
543        profile_id: &str,
544        request_id: u64,
545    ) -> Result<(), Error> {
546        if self.inner.is_null() {
547            return Err(Error::ConnectionFailed);
548        }
549        let profile_id = CString::new(profile_id).map_err(|_| Error::InvalidInput)?;
550        if unsafe {
551            cbf_bridge_client_open_default_prompt_ui(self.inner, profile_id.as_ptr(), request_id)
552        } {
553            Ok(())
554        } else {
555            Err(Error::ConnectionFailed)
556        }
557    }
558
559    /// Respond to a pending chrome-specific PromptUi request.
560    pub fn respond_prompt_ui(
561        &mut self,
562        profile_id: &str,
563        request_id: u64,
564        response: &PromptUiResponse,
565    ) -> Result<(), Error> {
566        if self.inner.is_null() {
567            return Err(Error::ConnectionFailed);
568        }
569        let profile_id = CString::new(profile_id).map_err(|_| Error::InvalidInput)?;
570        let (prompt_ui_kind, proceed, destination_path, report_abuse) = match response {
571            PromptUiResponse::PermissionPrompt { allow } => {
572                (CBF_PROMPT_UI_KIND_PERMISSION_PROMPT, *allow, None, false)
573            }
574            PromptUiResponse::DownloadPrompt {
575                allow,
576                destination_path,
577            } => (
578                CBF_PROMPT_UI_KIND_DOWNLOAD_PROMPT,
579                *allow,
580                to_optional_cstring(destination_path)?,
581                false,
582            ),
583            PromptUiResponse::ExtensionInstallPrompt { proceed } => (
584                CBF_PROMPT_UI_KIND_EXTENSION_INSTALL_PROMPT,
585                *proceed,
586                None,
587                false,
588            ),
589            PromptUiResponse::ExtensionUninstallPrompt {
590                proceed,
591                report_abuse,
592            } => (
593                CBF_PROMPT_UI_KIND_EXTENSION_UNINSTALL_PROMPT,
594                *proceed,
595                None,
596                *report_abuse,
597            ),
598            PromptUiResponse::PrintPreviewDialog { proceed } => (
599                CBF_PROMPT_UI_KIND_PRINT_PREVIEW_DIALOG,
600                *proceed,
601                None,
602                false,
603            ),
604            PromptUiResponse::Unknown => (CBF_PROMPT_UI_KIND_UNKNOWN, false, None, false),
605        };
606        if unsafe {
607            cbf_bridge_client_respond_prompt_ui(
608                self.inner,
609                profile_id.as_ptr(),
610                request_id,
611                prompt_ui_kind,
612                proceed,
613                report_abuse,
614                destination_path
615                    .as_ref()
616                    .map_or(ptr::null(), |path| path.as_ptr()),
617            )
618        } {
619            Ok(())
620        } else {
621            Err(Error::ConnectionFailed)
622        }
623    }
624
625    /// Respond to a page-originated prompt by resolving profile on the bridge side.
626    pub fn respond_prompt_ui_for_tab(
627        &mut self,
628        browsing_context_id: TabId,
629        request_id: u64,
630        response: &PromptUiResponse,
631    ) -> Result<(), Error> {
632        if self.inner.is_null() {
633            return Err(Error::ConnectionFailed);
634        }
635        let (prompt_ui_kind, proceed, destination_path, report_abuse) = match response {
636            PromptUiResponse::PermissionPrompt { allow } => {
637                (CBF_PROMPT_UI_KIND_PERMISSION_PROMPT, *allow, None, false)
638            }
639            PromptUiResponse::DownloadPrompt {
640                allow,
641                destination_path,
642            } => (
643                CBF_PROMPT_UI_KIND_DOWNLOAD_PROMPT,
644                *allow,
645                to_optional_cstring(destination_path)?,
646                false,
647            ),
648            PromptUiResponse::ExtensionInstallPrompt { proceed } => (
649                CBF_PROMPT_UI_KIND_EXTENSION_INSTALL_PROMPT,
650                *proceed,
651                None,
652                false,
653            ),
654            PromptUiResponse::ExtensionUninstallPrompt {
655                proceed,
656                report_abuse,
657            } => (
658                CBF_PROMPT_UI_KIND_EXTENSION_UNINSTALL_PROMPT,
659                *proceed,
660                None,
661                *report_abuse,
662            ),
663            PromptUiResponse::PrintPreviewDialog { proceed } => (
664                CBF_PROMPT_UI_KIND_PRINT_PREVIEW_DIALOG,
665                *proceed,
666                None,
667                false,
668            ),
669            PromptUiResponse::Unknown => (CBF_PROMPT_UI_KIND_UNKNOWN, false, None, false),
670        };
671        if unsafe {
672            cbf_bridge_client_respond_prompt_ui_for_tab(
673                self.inner,
674                browsing_context_id.get(),
675                request_id,
676                prompt_ui_kind,
677                proceed,
678                report_abuse,
679                destination_path
680                    .as_ref()
681                    .map_or(ptr::null(), |path| path.as_ptr()),
682            )
683        } {
684            Ok(())
685        } else {
686            Err(Error::ConnectionFailed)
687        }
688    }
689
690    /// Close a backend-managed PromptUi surface.
691    pub fn close_prompt_ui(
692        &mut self,
693        profile_id: &str,
694        prompt_ui_id: PromptUiId,
695    ) -> Result<(), Error> {
696        if self.inner.is_null() {
697            return Err(Error::ConnectionFailed);
698        }
699        let profile_id = CString::new(profile_id).map_err(|_| Error::InvalidInput)?;
700        if unsafe {
701            cbf_bridge_client_close_prompt_ui(self.inner, profile_id.as_ptr(), prompt_ui_id.get())
702        } {
703            Ok(())
704        } else {
705            Err(Error::ConnectionFailed)
706        }
707    }
708
709    /// Pause an in-progress download.
710    pub fn pause_download(&mut self, download_id: ChromeDownloadId) -> Result<(), Error> {
711        if self.inner.is_null() {
712            return Err(Error::ConnectionFailed);
713        }
714        if unsafe { cbf_bridge_client_pause_download(self.inner, download_id.get()) } {
715            Ok(())
716        } else {
717            Err(Error::ConnectionFailed)
718        }
719    }
720
721    /// Resume a paused download.
722    pub fn resume_download(&mut self, download_id: ChromeDownloadId) -> Result<(), Error> {
723        if self.inner.is_null() {
724            return Err(Error::ConnectionFailed);
725        }
726        if unsafe { cbf_bridge_client_resume_download(self.inner, download_id.get()) } {
727            Ok(())
728        } else {
729            Err(Error::ConnectionFailed)
730        }
731    }
732
733    /// Cancel an active download.
734    pub fn cancel_download(&mut self, download_id: ChromeDownloadId) -> Result<(), Error> {
735        if self.inner.is_null() {
736            return Err(Error::ConnectionFailed);
737        }
738        if unsafe { cbf_bridge_client_cancel_download(self.inner, download_id.get()) } {
739            Ok(())
740        } else {
741            Err(Error::ConnectionFailed)
742        }
743    }
744
745    /// Respond to host-mediated tab-open request.
746    pub fn respond_tab_open(
747        &mut self,
748        request_id: u64,
749        response: &ChromeBrowsingContextOpenResponse,
750    ) -> Result<(), Error> {
751        if self.inner.is_null() {
752            return Err(Error::ConnectionFailed);
753        }
754        let (response_kind, target_tab_id, activate) = match response {
755            ChromeBrowsingContextOpenResponse::AllowNewContext { activate } => {
756                (CBF_TAB_OPEN_RESPONSE_ALLOW_NEW_CONTEXT, 0, *activate)
757            }
758            ChromeBrowsingContextOpenResponse::AllowExistingContext { tab_id, activate } => (
759                CBF_TAB_OPEN_RESPONSE_ALLOW_EXISTING_CONTEXT,
760                tab_id.get(),
761                *activate,
762            ),
763            ChromeBrowsingContextOpenResponse::Deny => (CBF_TAB_OPEN_RESPONSE_DENY, 0, false),
764        };
765        if unsafe {
766            cbf_bridge_client_respond_tab_open(
767                self.inner,
768                request_id,
769                response_kind,
770                target_tab_id,
771                activate,
772            )
773        } {
774            Ok(())
775        } else {
776            Err(Error::ConnectionFailed)
777        }
778    }
779
780    /// Respond to host-mediated window open request.
781    ///
782    /// Current bridge path reuses tab-open response semantics.
783    pub fn respond_window_open(
784        &mut self,
785        request_id: u64,
786        response: &WindowOpenResponse,
787    ) -> Result<(), Error> {
788        let tab_open_response = match response {
789            WindowOpenResponse::AllowExistingWindow { .. }
790            | WindowOpenResponse::AllowNewWindow { .. } => {
791                ChromeBrowsingContextOpenResponse::AllowNewContext { activate: true }
792            }
793            WindowOpenResponse::Deny => ChromeBrowsingContextOpenResponse::Deny,
794        };
795        self.respond_tab_open(request_id, &tab_open_response)
796    }
797
798    /// Send a Chromium-shaped keyboard event to the page.
799    pub fn send_key_event_raw(
800        &mut self,
801        browsing_context_id: TabId,
802        event: &ChromeKeyEvent,
803        commands: &[String],
804    ) -> Result<(), Error> {
805        if self.inner.is_null() {
806            return Err(Error::ConnectionFailed);
807        }
808
809        let dom_code = to_optional_cstring(&event.dom_code)?;
810        let dom_key = to_optional_cstring(&event.dom_key)?;
811        let text = to_optional_cstring(&event.text)?;
812        let unmodified_text = to_optional_cstring(&event.unmodified_text)?;
813
814        let command_cstrings = commands
815            .iter()
816            .map(|command| CString::new(command.as_str()).map_err(|_| Error::InvalidInput))
817            .collect::<Result<Vec<_>, _>>()?;
818        let command_ptrs: Vec<*const std::os::raw::c_char> =
819            command_cstrings.iter().map(|cstr| cstr.as_ptr()).collect();
820
821        let ffi_event = CbfKeyEvent {
822            tab_id: browsing_context_id.get(),
823            type_: key_event_type_to_ffi(event.type_),
824            modifiers: event.modifiers,
825            windows_key_code: event.windows_key_code,
826            native_key_code: event.native_key_code,
827            dom_code: dom_code.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
828            dom_key: dom_key.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
829            text: text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
830            unmodified_text: unmodified_text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
831            auto_repeat: event.auto_repeat,
832            is_keypad: event.is_keypad,
833            is_system_key: event.is_system_key,
834            location: event.location,
835        };
836
837        let ffi_commands = CbfCommandList {
838            items: if command_ptrs.is_empty() {
839                ptr::null()
840            } else {
841                command_ptrs.as_ptr()
842            },
843            len: command_ptrs.len() as u32,
844        };
845
846        if unsafe { cbf_bridge_client_send_key_event(self.inner, &ffi_event, &ffi_commands) } {
847            Ok(())
848        } else {
849            Err(Error::ConnectionFailed)
850        }
851    }
852
853    /// Send a Chromium-shaped keyboard event to an extension popup.
854    pub fn send_extension_popup_key_event_raw(
855        &mut self,
856        popup_id: PopupId,
857        event: &ChromeKeyEvent,
858        commands: &[String],
859    ) -> Result<(), Error> {
860        if self.inner.is_null() {
861            return Err(Error::ConnectionFailed);
862        }
863
864        let dom_code = to_optional_cstring(&event.dom_code)?;
865        let dom_key = to_optional_cstring(&event.dom_key)?;
866        let text = to_optional_cstring(&event.text)?;
867        let unmodified_text = to_optional_cstring(&event.unmodified_text)?;
868
869        let command_cstrings = commands
870            .iter()
871            .map(|command| CString::new(command.as_str()).map_err(|_| Error::InvalidInput))
872            .collect::<Result<Vec<_>, _>>()?;
873        let command_ptrs: Vec<*const std::os::raw::c_char> =
874            command_cstrings.iter().map(|cstr| cstr.as_ptr()).collect();
875
876        let ffi_event = CbfKeyEvent {
877            tab_id: 0,
878            type_: key_event_type_to_ffi(event.type_),
879            modifiers: event.modifiers,
880            windows_key_code: event.windows_key_code,
881            native_key_code: event.native_key_code,
882            dom_code: dom_code.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
883            dom_key: dom_key.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
884            text: text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
885            unmodified_text: unmodified_text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
886            auto_repeat: event.auto_repeat,
887            is_keypad: event.is_keypad,
888            is_system_key: event.is_system_key,
889            location: event.location,
890        };
891
892        let ffi_commands = CbfCommandList {
893            items: if command_ptrs.is_empty() {
894                ptr::null()
895            } else {
896                command_ptrs.as_ptr()
897            },
898            len: command_ptrs.len() as u32,
899        };
900
901        if unsafe {
902            cbf_bridge_client_send_extension_popup_key_event(
903                self.inner,
904                popup_id.get(),
905                &ffi_event,
906                &ffi_commands,
907            )
908        } {
909            Ok(())
910        } else {
911            Err(Error::ConnectionFailed)
912        }
913    }
914
915    /// Send a mouse event to the page.
916    pub fn send_mouse_event(
917        &mut self,
918        browsing_context_id: TabId,
919        event: &ChromeMouseEvent,
920    ) -> Result<(), Error> {
921        if self.inner.is_null() {
922            return Err(Error::ConnectionFailed);
923        }
924
925        let ffi_event = CbfMouseEvent {
926            tab_id: browsing_context_id.get(),
927            type_: mouse_event_type_to_ffi(event.type_),
928            modifiers: event.modifiers,
929            button: mouse_button_to_ffi(event.button),
930            click_count: event.click_count,
931            position_in_widget_x: event.position_in_widget_x,
932            position_in_widget_y: event.position_in_widget_y,
933            position_in_screen_x: event.position_in_screen_x,
934            position_in_screen_y: event.position_in_screen_y,
935            movement_x: event.movement_x,
936            movement_y: event.movement_y,
937            is_raw_movement_event: event.is_raw_movement_event,
938            pointer_type: pointer_type_to_ffi(event.pointer_type),
939        };
940
941        if unsafe { cbf_bridge_client_send_mouse_event(self.inner, &ffi_event) } {
942            Ok(())
943        } else {
944            Err(Error::ConnectionFailed)
945        }
946    }
947
948    /// Send a mouse event to an extension popup.
949    pub fn send_extension_popup_mouse_event(
950        &mut self,
951        popup_id: PopupId,
952        event: &ChromeMouseEvent,
953    ) -> Result<(), Error> {
954        if self.inner.is_null() {
955            return Err(Error::ConnectionFailed);
956        }
957
958        let ffi_event = CbfMouseEvent {
959            tab_id: 0,
960            type_: mouse_event_type_to_ffi(event.type_),
961            modifiers: event.modifiers,
962            button: mouse_button_to_ffi(event.button),
963            click_count: event.click_count,
964            position_in_widget_x: event.position_in_widget_x,
965            position_in_widget_y: event.position_in_widget_y,
966            position_in_screen_x: event.position_in_screen_x,
967            position_in_screen_y: event.position_in_screen_y,
968            movement_x: event.movement_x,
969            movement_y: event.movement_y,
970            is_raw_movement_event: event.is_raw_movement_event,
971            pointer_type: pointer_type_to_ffi(event.pointer_type),
972        };
973
974        if unsafe {
975            cbf_bridge_client_send_extension_popup_mouse_event(
976                self.inner,
977                popup_id.get(),
978                &ffi_event,
979            )
980        } {
981            Ok(())
982        } else {
983            Err(Error::ConnectionFailed)
984        }
985    }
986
987    /// Send a Chromium-shaped mouse wheel event to the page.
988    pub fn send_mouse_wheel_event_raw(
989        &mut self,
990        browsing_context_id: TabId,
991        event: &ChromeMouseWheelEvent,
992    ) -> Result<(), Error> {
993        if self.inner.is_null() {
994            return Err(Error::ConnectionFailed);
995        }
996
997        let ffi_event = CbfMouseWheelEvent {
998            tab_id: browsing_context_id.get(),
999            modifiers: event.modifiers,
1000            position_in_widget_x: event.position_in_widget_x,
1001            position_in_widget_y: event.position_in_widget_y,
1002            position_in_screen_x: event.position_in_screen_x,
1003            position_in_screen_y: event.position_in_screen_y,
1004            movement_x: event.movement_x,
1005            movement_y: event.movement_y,
1006            is_raw_movement_event: event.is_raw_movement_event,
1007            delta_x: event.delta_x,
1008            delta_y: event.delta_y,
1009            wheel_ticks_x: event.wheel_ticks_x,
1010            wheel_ticks_y: event.wheel_ticks_y,
1011            phase: event.phase,
1012            momentum_phase: event.momentum_phase,
1013            delta_units: scroll_granularity_to_ffi(event.delta_units),
1014        };
1015
1016        if unsafe { cbf_bridge_client_send_mouse_wheel_event(self.inner, &ffi_event) } {
1017            Ok(())
1018        } else {
1019            Err(Error::ConnectionFailed)
1020        }
1021    }
1022
1023    /// Send a Chromium-shaped mouse wheel event to an extension popup.
1024    pub fn send_extension_popup_mouse_wheel_event_raw(
1025        &mut self,
1026        popup_id: PopupId,
1027        event: &ChromeMouseWheelEvent,
1028    ) -> Result<(), Error> {
1029        if self.inner.is_null() {
1030            return Err(Error::ConnectionFailed);
1031        }
1032
1033        let ffi_event = CbfMouseWheelEvent {
1034            tab_id: 0,
1035            modifiers: event.modifiers,
1036            position_in_widget_x: event.position_in_widget_x,
1037            position_in_widget_y: event.position_in_widget_y,
1038            position_in_screen_x: event.position_in_screen_x,
1039            position_in_screen_y: event.position_in_screen_y,
1040            movement_x: event.movement_x,
1041            movement_y: event.movement_y,
1042            is_raw_movement_event: event.is_raw_movement_event,
1043            delta_x: event.delta_x,
1044            delta_y: event.delta_y,
1045            wheel_ticks_x: event.wheel_ticks_x,
1046            wheel_ticks_y: event.wheel_ticks_y,
1047            phase: event.phase,
1048            momentum_phase: event.momentum_phase,
1049            delta_units: scroll_granularity_to_ffi(event.delta_units),
1050        };
1051
1052        if unsafe {
1053            cbf_bridge_client_send_extension_popup_mouse_wheel_event(
1054                self.inner,
1055                popup_id.get(),
1056                &ffi_event,
1057            )
1058        } {
1059            Ok(())
1060        } else {
1061            Err(Error::ConnectionFailed)
1062        }
1063    }
1064
1065    /// Send a drag update event for host-owned drag session.
1066    pub fn send_drag_update(&mut self, update: &ChromeDragUpdate) -> Result<(), Error> {
1067        if self.inner.is_null() {
1068            return Err(Error::ConnectionFailed);
1069        }
1070
1071        let ffi_update = CbfDragUpdate {
1072            session_id: update.session_id,
1073            tab_id: update.browsing_context_id.get(),
1074            allowed_operations: update.allowed_operations.bits(),
1075            modifiers: update.modifiers,
1076            position_in_widget_x: update.position_in_widget_x,
1077            position_in_widget_y: update.position_in_widget_y,
1078            position_in_screen_x: update.position_in_screen_x,
1079            position_in_screen_y: update.position_in_screen_y,
1080        };
1081
1082        if unsafe { cbf_bridge_client_send_drag_update(self.inner, &ffi_update) } {
1083            Ok(())
1084        } else {
1085            Err(Error::ConnectionFailed)
1086        }
1087    }
1088
1089    /// Send a drag drop event for host-owned drag session.
1090    pub fn send_drag_drop(&mut self, drop: &ChromeDragDrop) -> Result<(), Error> {
1091        if self.inner.is_null() {
1092            return Err(Error::ConnectionFailed);
1093        }
1094
1095        let ffi_drop = CbfDragDrop {
1096            session_id: drop.session_id,
1097            tab_id: drop.browsing_context_id.get(),
1098            modifiers: drop.modifiers,
1099            position_in_widget_x: drop.position_in_widget_x,
1100            position_in_widget_y: drop.position_in_widget_y,
1101            position_in_screen_x: drop.position_in_screen_x,
1102            position_in_screen_y: drop.position_in_screen_y,
1103        };
1104
1105        if unsafe { cbf_bridge_client_send_drag_drop(self.inner, &ffi_drop) } {
1106            Ok(())
1107        } else {
1108            Err(Error::ConnectionFailed)
1109        }
1110    }
1111
1112    /// Cancel a host-owned drag session.
1113    pub fn send_drag_cancel(
1114        &mut self,
1115        session_id: u64,
1116        browsing_context_id: TabId,
1117    ) -> Result<(), Error> {
1118        if self.inner.is_null() {
1119            return Err(Error::ConnectionFailed);
1120        }
1121
1122        if unsafe {
1123            cbf_bridge_client_send_drag_cancel(self.inner, session_id, browsing_context_id.get())
1124        } {
1125            Ok(())
1126        } else {
1127            Err(Error::ConnectionFailed)
1128        }
1129    }
1130
1131    /// Update the IME composition state.
1132    pub fn set_composition(&mut self, composition: &ChromeImeComposition) -> Result<(), Error> {
1133        if self.inner.is_null() {
1134            return Err(Error::ConnectionFailed);
1135        }
1136
1137        let text = CString::new(composition.text.as_str()).map_err(|_| Error::InvalidInput)?;
1138        let spans = to_ffi_ime_text_spans(&composition.spans);
1139        let span_list = CbfImeTextSpanList {
1140            items: if spans.is_empty() {
1141                ptr::null()
1142            } else {
1143                spans.as_ptr()
1144            },
1145            len: spans.len() as u32,
1146        };
1147        let (replacement_start, replacement_end) = ime_range_to_ffi(&composition.replacement_range);
1148
1149        let ffi_composition = CbfImeComposition {
1150            tab_id: composition.browsing_context_id.get(),
1151            text: text.as_ptr(),
1152            selection_start: composition.selection_start,
1153            selection_end: composition.selection_end,
1154            replacement_range_start: replacement_start,
1155            replacement_range_end: replacement_end,
1156            spans: span_list,
1157        };
1158
1159        if unsafe { cbf_bridge_client_set_composition(self.inner, &ffi_composition) } {
1160            Ok(())
1161        } else {
1162            Err(Error::ConnectionFailed)
1163        }
1164    }
1165
1166    /// Update the IME composition state for an extension popup.
1167    pub fn set_extension_popup_composition(
1168        &mut self,
1169        composition: &ChromeTransientImeComposition,
1170    ) -> Result<(), Error> {
1171        if self.inner.is_null() {
1172            return Err(Error::ConnectionFailed);
1173        }
1174
1175        let text = CString::new(composition.text.as_str()).map_err(|_| Error::InvalidInput)?;
1176        let spans = to_ffi_ime_text_spans(&composition.spans);
1177        let span_list = CbfImeTextSpanList {
1178            items: if spans.is_empty() {
1179                ptr::null()
1180            } else {
1181                spans.as_ptr()
1182            },
1183            len: spans.len() as u32,
1184        };
1185        let (replacement_start, replacement_end) = ime_range_to_ffi(&composition.replacement_range);
1186
1187        let ffi_composition = CbfImeComposition {
1188            tab_id: 0,
1189            text: text.as_ptr(),
1190            selection_start: composition.selection_start,
1191            selection_end: composition.selection_end,
1192            replacement_range_start: replacement_start,
1193            replacement_range_end: replacement_end,
1194            spans: span_list,
1195        };
1196
1197        if unsafe {
1198            cbf_bridge_client_set_extension_popup_composition(
1199                self.inner,
1200                composition.popup_id.get(),
1201                &ffi_composition,
1202            )
1203        } {
1204            Ok(())
1205        } else {
1206            Err(Error::ConnectionFailed)
1207        }
1208    }
1209
1210    /// Commit IME text input to the page.
1211    pub fn commit_text(&mut self, commit: &ChromeImeCommitText) -> Result<(), Error> {
1212        if self.inner.is_null() {
1213            return Err(Error::ConnectionFailed);
1214        }
1215
1216        let text = CString::new(commit.text.as_str()).map_err(|_| Error::InvalidInput)?;
1217        let spans = to_ffi_ime_text_spans(&commit.spans);
1218        let span_list = CbfImeTextSpanList {
1219            items: if spans.is_empty() {
1220                ptr::null()
1221            } else {
1222                spans.as_ptr()
1223            },
1224            len: spans.len() as u32,
1225        };
1226        let (replacement_start, replacement_end) = ime_range_to_ffi(&commit.replacement_range);
1227
1228        let ffi_commit = CbfImeCommitText {
1229            tab_id: commit.browsing_context_id.get(),
1230            text: text.as_ptr(),
1231            relative_caret_position: commit.relative_caret_position,
1232            replacement_range_start: replacement_start,
1233            replacement_range_end: replacement_end,
1234            spans: span_list,
1235        };
1236
1237        if unsafe { cbf_bridge_client_commit_text(self.inner, &ffi_commit) } {
1238            Ok(())
1239        } else {
1240            Err(Error::ConnectionFailed)
1241        }
1242    }
1243
1244    /// Commit IME text input to an extension popup.
1245    pub fn commit_extension_popup_text(
1246        &mut self,
1247        commit: &ChromeTransientImeCommitText,
1248    ) -> Result<(), Error> {
1249        if self.inner.is_null() {
1250            return Err(Error::ConnectionFailed);
1251        }
1252
1253        let text = CString::new(commit.text.as_str()).map_err(|_| Error::InvalidInput)?;
1254        let spans = to_ffi_ime_text_spans(&commit.spans);
1255        let span_list = CbfImeTextSpanList {
1256            items: if spans.is_empty() {
1257                ptr::null()
1258            } else {
1259                spans.as_ptr()
1260            },
1261            len: spans.len() as u32,
1262        };
1263        let (replacement_start, replacement_end) = ime_range_to_ffi(&commit.replacement_range);
1264
1265        let ffi_commit = CbfImeCommitText {
1266            tab_id: 0,
1267            text: text.as_ptr(),
1268            relative_caret_position: commit.relative_caret_position,
1269            replacement_range_start: replacement_start,
1270            replacement_range_end: replacement_end,
1271            spans: span_list,
1272        };
1273
1274        if unsafe {
1275            cbf_bridge_client_commit_extension_popup_text(
1276                self.inner,
1277                commit.popup_id.get(),
1278                &ffi_commit,
1279            )
1280        } {
1281            Ok(())
1282        } else {
1283            Err(Error::ConnectionFailed)
1284        }
1285    }
1286
1287    /// Finish composing IME text with the specified behavior.
1288    pub fn finish_composing_text(
1289        &mut self,
1290        browsing_context_id: TabId,
1291        behavior: ChromeConfirmCompositionBehavior,
1292    ) -> Result<(), Error> {
1293        if self.inner.is_null() {
1294            return Err(Error::ConnectionFailed);
1295        }
1296
1297        let behavior = match behavior {
1298            ChromeConfirmCompositionBehavior::DoNotKeepSelection => {
1299                CBF_IME_CONFIRM_DO_NOT_KEEP_SELECTION
1300            }
1301            ChromeConfirmCompositionBehavior::KeepSelection => CBF_IME_CONFIRM_KEEP_SELECTION,
1302        };
1303
1304        if unsafe {
1305            cbf_bridge_client_finish_composing_text(self.inner, browsing_context_id.get(), behavior)
1306        } {
1307            Ok(())
1308        } else {
1309            Err(Error::ConnectionFailed)
1310        }
1311    }
1312
1313    /// Finish composing IME text inside an extension popup.
1314    pub fn finish_extension_popup_composing_text(
1315        &mut self,
1316        popup_id: PopupId,
1317        behavior: ChromeConfirmCompositionBehavior,
1318    ) -> Result<(), Error> {
1319        if self.inner.is_null() {
1320            return Err(Error::ConnectionFailed);
1321        }
1322
1323        let behavior = match behavior {
1324            ChromeConfirmCompositionBehavior::DoNotKeepSelection => {
1325                CBF_IME_CONFIRM_DO_NOT_KEEP_SELECTION
1326            }
1327            ChromeConfirmCompositionBehavior::KeepSelection => CBF_IME_CONFIRM_KEEP_SELECTION,
1328        };
1329
1330        if unsafe {
1331            cbf_bridge_client_finish_extension_popup_composing_text(
1332                self.inner,
1333                popup_id.get(),
1334                behavior,
1335            )
1336        } {
1337            Ok(())
1338        } else {
1339            Err(Error::ConnectionFailed)
1340        }
1341    }
1342
1343    pub fn set_extension_popup_focus(
1344        &mut self,
1345        popup_id: PopupId,
1346        focused: bool,
1347    ) -> Result<(), Error> {
1348        if self.inner.is_null() {
1349            return Err(Error::ConnectionFailed);
1350        }
1351
1352        if unsafe {
1353            cbf_bridge_client_set_extension_popup_focus(self.inner, popup_id.get(), focused)
1354        } {
1355            Ok(())
1356        } else {
1357            Err(Error::ConnectionFailed)
1358        }
1359    }
1360
1361    pub fn set_extension_popup_size(
1362        &mut self,
1363        popup_id: PopupId,
1364        width: u32,
1365        height: u32,
1366    ) -> Result<(), Error> {
1367        if self.inner.is_null() {
1368            return Err(Error::ConnectionFailed);
1369        }
1370
1371        if unsafe {
1372            cbf_bridge_client_set_extension_popup_size(self.inner, popup_id.get(), width, height)
1373        } {
1374            Ok(())
1375        } else {
1376            Err(Error::ConnectionFailed)
1377        }
1378    }
1379
1380    pub fn close_extension_popup(&mut self, popup_id: PopupId) -> Result<(), Error> {
1381        if self.inner.is_null() {
1382            return Err(Error::ConnectionFailed);
1383        }
1384
1385        if unsafe { cbf_bridge_client_close_extension_popup(self.inner, popup_id.get()) } {
1386            Ok(())
1387        } else {
1388            Err(Error::ConnectionFailed)
1389        }
1390    }
1391
1392    /// Execute a browser-generic edit action for the given page.
1393    pub fn execute_edit_action(
1394        &mut self,
1395        browsing_context_id: TabId,
1396        action: EditAction,
1397    ) -> Result<(), Error> {
1398        if self.inner.is_null() {
1399            return Err(Error::ConnectionFailed);
1400        }
1401
1402        if unsafe {
1403            cbf_bridge_client_execute_edit_action(
1404                self.inner,
1405                browsing_context_id.get(),
1406                edit_action_to_ffi(action),
1407            )
1408        } {
1409            Ok(())
1410        } else {
1411            Err(Error::ConnectionFailed)
1412        }
1413    }
1414
1415    /// Execute a browser-generic edit action for the given extension popup.
1416    pub fn execute_extension_popup_edit_action(
1417        &mut self,
1418        popup_id: PopupId,
1419        action: EditAction,
1420    ) -> Result<(), Error> {
1421        if self.inner.is_null() {
1422            return Err(Error::ConnectionFailed);
1423        }
1424
1425        if unsafe {
1426            cbf_bridge_client_execute_extension_popup_edit_action(
1427                self.inner,
1428                popup_id.get(),
1429                edit_action_to_ffi(action),
1430            )
1431        } {
1432            Ok(())
1433        } else {
1434            Err(Error::ConnectionFailed)
1435        }
1436    }
1437
1438    /// Execute a context menu command for the given menu.
1439    pub fn execute_context_menu_command(
1440        &mut self,
1441        menu_id: u64,
1442        command_id: i32,
1443        event_flags: i32,
1444    ) -> Result<(), Error> {
1445        if self.inner.is_null() {
1446            return Err(Error::ConnectionFailed);
1447        }
1448
1449        if unsafe {
1450            cbf_bridge_client_execute_context_menu_command(
1451                self.inner,
1452                menu_id,
1453                command_id,
1454                event_flags,
1455            )
1456        } {
1457            Ok(())
1458        } else {
1459            Err(Error::ConnectionFailed)
1460        }
1461    }
1462
1463    /// Accept a host-owned choice menu selection.
1464    pub fn accept_choice_menu_selection(
1465        &mut self,
1466        request_id: u64,
1467        indices: &[i32],
1468    ) -> Result<(), Error> {
1469        if self.inner.is_null() {
1470            return Err(Error::ConnectionFailed);
1471        }
1472
1473        let ffi_indices = CbfChoiceMenuSelectedIndices {
1474            items: indices.as_ptr(),
1475            len: indices.len() as u32,
1476        };
1477        if unsafe {
1478            cbf_bridge_client_accept_choice_menu_selection(self.inner, request_id, &ffi_indices)
1479        } {
1480            Ok(())
1481        } else {
1482            Err(Error::ConnectionFailed)
1483        }
1484    }
1485
1486    /// Dismiss a host-owned choice menu without a selection.
1487    pub fn dismiss_choice_menu(&mut self, request_id: u64) -> Result<(), Error> {
1488        if self.inner.is_null() {
1489            return Err(Error::ConnectionFailed);
1490        }
1491
1492        if unsafe { cbf_bridge_client_dismiss_choice_menu(self.inner, request_id) } {
1493            Ok(())
1494        } else {
1495            Err(Error::ConnectionFailed)
1496        }
1497    }
1498
1499    /// Dismiss the context menu with the given id.
1500    pub fn dismiss_context_menu(&mut self, menu_id: u64) -> Result<(), Error> {
1501        if self.inner.is_null() {
1502            return Err(Error::ConnectionFailed);
1503        }
1504
1505        if unsafe { cbf_bridge_client_dismiss_context_menu(self.inner, menu_id) } {
1506            Ok(())
1507        } else {
1508            Err(Error::ConnectionFailed)
1509        }
1510    }
1511
1512    /// Request a graceful shutdown from the backend.
1513    pub fn request_shutdown(&mut self, request_id: u64) -> Result<(), Error> {
1514        if self.inner.is_null() {
1515            return Err(Error::ConnectionFailed);
1516        }
1517
1518        if unsafe { cbf_bridge_client_request_shutdown(self.inner, request_id) } {
1519            Ok(())
1520        } else {
1521            Err(Error::ConnectionFailed)
1522        }
1523    }
1524
1525    /// Respond to a shutdown confirmation request.
1526    pub fn confirm_shutdown(&mut self, request_id: u64, proceed: bool) -> Result<(), Error> {
1527        if self.inner.is_null() {
1528            return Err(Error::ConnectionFailed);
1529        }
1530
1531        if unsafe { cbf_bridge_client_confirm_shutdown(self.inner, request_id, proceed) } {
1532            Ok(())
1533        } else {
1534            Err(Error::ConnectionFailed)
1535        }
1536    }
1537
1538    /// Force an immediate shutdown without confirmations.
1539    pub fn force_shutdown(&mut self) -> Result<(), Error> {
1540        if self.inner.is_null() {
1541            return Err(Error::ConnectionFailed);
1542        }
1543
1544        if unsafe { cbf_bridge_client_force_shutdown(self.inner) } {
1545            Ok(())
1546        } else {
1547            Err(Error::ConnectionFailed)
1548        }
1549    }
1550
1551    /// Tear down the IPC client and free native resources.
1552    pub fn shutdown(&mut self) {
1553        if !self.inner.is_null() {
1554            unsafe { cbf_bridge_client_shutdown(self.inner) };
1555        }
1556    }
1557}
1558
1559impl IpcEventWaitHandle {
1560    pub(crate) fn wait_for_event(
1561        &self,
1562        timeout: Option<Duration>,
1563    ) -> Result<EventWaitResult, Error> {
1564        wait_for_event_inner(self.inner, timeout)
1565    }
1566}
1567
1568fn wait_for_event_inner(
1569    inner: *mut CbfBridgeClientHandle,
1570    timeout: Option<Duration>,
1571) -> Result<EventWaitResult, Error> {
1572    if inner.is_null() {
1573        return Ok(EventWaitResult::Closed);
1574    }
1575
1576    let timeout_ms = timeout
1577        .map(|value| value.as_millis().min(i64::MAX as u128) as i64)
1578        .unwrap_or(-1);
1579    let status = unsafe { cbf_bridge_client_wait_for_event(inner, timeout_ms) };
1580
1581    match status {
1582        CBF_BRIDGE_EVENT_WAIT_STATUS_EVENT_AVAILABLE => Ok(EventWaitResult::EventAvailable),
1583        CBF_BRIDGE_EVENT_WAIT_STATUS_TIMED_OUT => Ok(EventWaitResult::TimedOut),
1584        CBF_BRIDGE_EVENT_WAIT_STATUS_DISCONNECTED => Ok(EventWaitResult::Disconnected),
1585        CBF_BRIDGE_EVENT_WAIT_STATUS_CLOSED => Ok(EventWaitResult::Closed),
1586        _ => Err(Error::InvalidEvent),
1587    }
1588}
1589
1590fn edit_action_to_ffi(action: EditAction) -> u8 {
1591    match action {
1592        EditAction::Undo => CBF_EDIT_ACTION_UNDO,
1593        EditAction::Redo => CBF_EDIT_ACTION_REDO,
1594        EditAction::Cut => CBF_EDIT_ACTION_CUT,
1595        EditAction::Copy => CBF_EDIT_ACTION_COPY,
1596        EditAction::Paste => CBF_EDIT_ACTION_PASTE,
1597        EditAction::SelectAll => CBF_EDIT_ACTION_SELECT_ALL,
1598    }
1599}
1600
1601impl Drop for IpcClient {
1602    fn drop(&mut self) {
1603        if !self.inner.is_null() {
1604            unsafe { cbf_bridge_client_destroy(self.inner) };
1605            self.inner = ptr::null_mut();
1606        }
1607    }
1608}