Skip to main content

pdfkit/
view_delegate.rs

1use std::ffi::{CStr, CString};
2use std::fmt;
3use std::os::raw::{c_char, c_void};
4use std::panic::{catch_unwind, AssertUnwindSafe};
5use std::ptr;
6
7use crate::action_remote_goto::PdfActionRemoteGoTo;
8use crate::error::Result;
9use crate::ffi;
10use crate::handle::ObjectHandle;
11use crate::view::PdfView;
12
13pub trait PdfViewDelegate: 'static {
14    fn handle_link_click(&mut self, _view: PdfView, _url: &str) -> bool {
15        false
16    }
17
18    fn will_change_scale_factor(&mut self, _view: PdfView, scale_factor: f64) -> f64 {
19        scale_factor.clamp(0.1, 10.0)
20    }
21
22    fn print_job_title(&mut self, _view: PdfView) -> Option<String> {
23        None
24    }
25
26    fn perform_print(&mut self, _view: PdfView) -> bool {
27        false
28    }
29
30    fn perform_find(&mut self, _view: PdfView) -> bool {
31        false
32    }
33
34    fn perform_go_to_page(&mut self, _view: PdfView) -> bool {
35        false
36    }
37
38    fn open_pdf_for_remote_goto_action(
39        &mut self,
40        _view: PdfView,
41        _action: PdfActionRemoteGoTo,
42    ) -> bool {
43        false
44    }
45}
46
47struct DelegateState {
48    delegate: Box<dyn PdfViewDelegate>,
49}
50
51pub struct PdfViewDelegateHandle {
52    handle: ObjectHandle,
53    _state: Box<DelegateState>,
54}
55
56impl PdfViewDelegateHandle {
57    pub fn new(delegate: impl PdfViewDelegate) -> Result<Self> {
58        let mut state = Box::new(DelegateState {
59            delegate: Box::new(delegate),
60        });
61        let context = ptr::addr_of_mut!(*state).cast::<c_void>();
62        let mut out_delegate = ptr::null_mut();
63        let mut out_error = ptr::null_mut();
64        let status = unsafe {
65            ffi::pdf_view_delegate_new(
66                context,
67                Some(pdf_view_delegate_link_click_trampoline),
68                Some(pdf_view_delegate_scale_factor_trampoline),
69                Some(pdf_view_delegate_print_job_title_trampoline),
70                Some(pdf_view_delegate_perform_print_trampoline),
71                Some(pdf_view_delegate_perform_find_trampoline),
72                Some(pdf_view_delegate_perform_go_to_page_trampoline),
73                Some(pdf_view_delegate_remote_goto_trampoline),
74                &mut out_delegate,
75                &mut out_error,
76            )
77        };
78        crate::util::status_result(status, out_error)?;
79        Ok(Self {
80            handle: crate::util::required_handle(out_delegate, "PDFViewDelegate")?,
81            _state: state,
82        })
83    }
84
85    pub(crate) fn as_handle_ptr(&self) -> *mut c_void {
86        self.handle.as_ptr()
87    }
88}
89
90impl fmt::Debug for PdfViewDelegateHandle {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.debug_struct("PdfViewDelegateHandle")
93            .finish_non_exhaustive()
94    }
95}
96
97fn duplicate_string(value: Option<String>) -> *mut c_char {
98    value
99        .and_then(|value| CString::new(value).ok())
100        .map_or(ptr::null_mut(), |value| unsafe {
101            libc::strdup(value.as_ptr())
102        })
103}
104
105/// # Safety
106/// The caller must ensure that `context` is either null or a valid pointer to `DelegateState`
107/// that was obtained from `Box::into_raw()` and has not yet been freed.
108unsafe fn delegate_state(context: *mut c_void) -> Option<&'static mut DelegateState> {
109    context.cast::<DelegateState>().as_mut()
110}
111
112/// Helper to convert a retained PDFView pointer to a PdfView.
113///
114/// # Safety
115/// `handle` must be either null or a valid, retained pointer to a PDFView object from Swift.
116unsafe fn retained_view(handle: *mut c_void) -> Option<PdfView> {
117    // SAFETY: caller guarantees valid handle or null
118    unsafe { ObjectHandle::from_retained_ptr(handle) }.map(PdfView::from_handle)
119}
120
121/// Helper to convert a retained PDFActionRemoteGoTo pointer to a PdfActionRemoteGoTo.
122///
123/// # Safety
124/// `handle` must be either null or a valid, retained pointer to a PDFActionRemoteGoTo object from Swift.
125unsafe fn retained_remote_goto_action(handle: *mut c_void) -> Option<PdfActionRemoteGoTo> {
126    // SAFETY: caller guarantees valid handle or null
127    unsafe { ObjectHandle::from_retained_ptr(handle) }.map(PdfActionRemoteGoTo::from_handle)
128}
129
130/// # Safety
131/// This is an extern "C" callback invoked by Swift. The caller must pass a valid, non-null
132/// `context` pointer that points to `DelegateState`, and `view_handle` must be a retained
133/// PDFView pointer from Swift (or null). The caller must pass a valid C string for `url`
134/// (or null). Panics are caught to prevent unwinding across the FFI boundary.
135unsafe extern "C" fn pdf_view_delegate_link_click_trampoline(
136    context: *mut c_void,
137    view_handle: *mut c_void,
138    url: *const c_char,
139) -> i32 {
140    catch_unwind(AssertUnwindSafe(|| {
141        // SAFETY: caller is responsible for providing valid context and view_handle pointers
142        let Some(state) = (unsafe { delegate_state(context) }) else {
143            return 0;
144        };
145        let Some(view) = (unsafe { retained_view(view_handle) }) else {
146            return 0;
147        };
148        let Some(url) =
149            (!url.is_null()).then(|| unsafe {
150                // SAFETY: checked for null; Swift guarantees valid C string
151                CStr::from_ptr(url).to_string_lossy().into_owned()
152            })
153        else {
154            return 0;
155        };
156        i32::from(state.delegate.handle_link_click(view, &url))
157    }))
158    .unwrap_or(0)
159}
160
161/// # Safety
162/// This is an extern "C" callback invoked by Swift. The caller must pass a valid, non-null
163/// `context` pointer that points to `DelegateState`, and `view_handle` must be a retained
164/// PDFView pointer from Swift (or null). Panics are caught to prevent unwinding across the
165/// FFI boundary.
166unsafe extern "C" fn pdf_view_delegate_scale_factor_trampoline(
167    context: *mut c_void,
168    view_handle: *mut c_void,
169    scale_factor: f64,
170) -> f64 {
171    catch_unwind(AssertUnwindSafe(|| {
172        // SAFETY: caller is responsible for providing valid context and view_handle pointers
173        let Some(state) = (unsafe { delegate_state(context) }) else {
174            return scale_factor.clamp(0.1, 10.0);
175        };
176        let Some(view) = (unsafe { retained_view(view_handle) }) else {
177            return scale_factor.clamp(0.1, 10.0);
178        };
179        state.delegate.will_change_scale_factor(view, scale_factor)
180    }))
181    .unwrap_or_else(|_| scale_factor.clamp(0.1, 10.0))
182}
183
184unsafe extern "C" fn pdf_view_delegate_print_job_title_trampoline(
185    context: *mut c_void,
186    view_handle: *mut c_void,
187) -> *mut c_char {
188    catch_unwind(AssertUnwindSafe(|| {
189        let Some(state) = (unsafe { delegate_state(context) }) else {
190            return ptr::null_mut();
191        };
192        let Some(view) = (unsafe { retained_view(view_handle) }) else {
193            return ptr::null_mut();
194        };
195        duplicate_string(state.delegate.print_job_title(view))
196    }))
197    .unwrap_or(ptr::null_mut())
198}
199
200unsafe extern "C" fn pdf_view_delegate_perform_print_trampoline(
201    context: *mut c_void,
202    view_handle: *mut c_void,
203) -> i32 {
204    catch_unwind(AssertUnwindSafe(|| {
205        let Some(state) = (unsafe { delegate_state(context) }) else {
206            return 0;
207        };
208        let Some(view) = (unsafe { retained_view(view_handle) }) else {
209            return 0;
210        };
211        i32::from(state.delegate.perform_print(view))
212    }))
213    .unwrap_or(0)
214}
215
216unsafe extern "C" fn pdf_view_delegate_perform_find_trampoline(
217    context: *mut c_void,
218    view_handle: *mut c_void,
219) -> i32 {
220    catch_unwind(AssertUnwindSafe(|| {
221        let Some(state) = (unsafe { delegate_state(context) }) else {
222            return 0;
223        };
224        let Some(view) = (unsafe { retained_view(view_handle) }) else {
225            return 0;
226        };
227        i32::from(state.delegate.perform_find(view))
228    }))
229    .unwrap_or(0)
230}
231
232unsafe extern "C" fn pdf_view_delegate_perform_go_to_page_trampoline(
233    context: *mut c_void,
234    view_handle: *mut c_void,
235) -> i32 {
236    catch_unwind(AssertUnwindSafe(|| {
237        let Some(state) = (unsafe { delegate_state(context) }) else {
238            return 0;
239        };
240        let Some(view) = (unsafe { retained_view(view_handle) }) else {
241            return 0;
242        };
243        i32::from(state.delegate.perform_go_to_page(view))
244    }))
245    .unwrap_or(0)
246}
247
248unsafe extern "C" fn pdf_view_delegate_remote_goto_trampoline(
249    context: *mut c_void,
250    view_handle: *mut c_void,
251    action_handle: *mut c_void,
252) -> i32 {
253    catch_unwind(AssertUnwindSafe(|| {
254        let Some(state) = (unsafe { delegate_state(context) }) else {
255            return 0;
256        };
257        let Some(view) = (unsafe { retained_view(view_handle) }) else {
258            return 0;
259        };
260        let Some(action) = (unsafe { retained_remote_goto_action(action_handle) }) else {
261            return 0;
262        };
263        i32::from(state.delegate.open_pdf_for_remote_goto_action(view, action))
264    }))
265    .unwrap_or(0)
266}