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