1
//! ABI-stable snapshot handles for the app's live resources.
2
//!
3
//! [`FontCacheSnapshot`] and [`ImageCacheSnapshot`] are `#[repr(C)]` boxed handles a callback
4
//! obtains via `CallbackInfo::get_font_cache_clone()` /
5
//! `CallbackInfo::get_image_cache_clone()` and hands to consumers that lay
6
//! content out OUTSIDE the window pipeline — `Pdf::from_styled_dom_with_resources`
7
//! being the canonical one ("print exactly what is on screen").
8
//!
9
//! Both are SNAPSHOT HANDLES, not live views: cloning is cheap because the
10
//! heavy state is refcounted (parsed font faces sit behind
11
//! `Arc<Mutex<HashMap<..>>>` shared by [`FontManager::clone_shared`]; decoded
12
//! image pixels sit behind refcounted `ImageRef`s), so nothing is re-parsed,
13
//! re-discovered or re-decoded — the point is speed, not memory. The handle
14
//! stays valid after the callback returns, so a print job can run off-thread.
15

            
16
use core::ffi::c_void;
17

            
18
#[cfg(feature = "text_layout")]
19
use azul_css::props::basic::FontRef;
20

            
21
#[cfg(feature = "text_layout")]
22
type InnerFontManager = crate::text3::cache::FontManager<FontRef>;
23

            
24
/// Boxed snapshot of the window's font resolution state: shared parsed-font
25
/// pool, resolved fallback chains, embedded/in-memory fonts, registry link.
26
///
27
/// Obtain via `CallbackInfo::get_font_cache_clone()`. `Clone` derives another
28
/// shared handle (cheap); dropping releases only this handle's box.
29
#[repr(C)]
30
#[derive(Debug)]
31
pub struct FontCacheSnapshot {
32
    /// Boxed [`FontManager`](crate::text3::cache::FontManager) (opaque over
33
    /// the ABI; null when the `text_layout` feature is compiled out).
34
    pub ptr: *mut c_void,
35
    /// Standard azul destructor latch: `false` on moved-out copies so only
36
    /// one side of an FFI move runs the destructor.
37
    pub run_destructor: bool,
38
}
39

            
40
impl FontCacheSnapshot {
41
    /// Wrap a font manager into an ABI handle.
42
    #[cfg(feature = "text_layout")]
43
    #[must_use] pub fn from_font_manager(fm: InnerFontManager) -> Self {
44
        Self {
45
            ptr: Box::into_raw(Box::new(fm)).cast(),
46
            run_destructor: true,
47
        }
48
    }
49

            
50
    /// Borrow the wrapped font manager, if any.
51
    #[cfg(feature = "text_layout")]
52
    #[must_use] pub const fn as_font_manager(&self) -> Option<&InnerFontManager> {
53
        unsafe { self.ptr.cast::<InnerFontManager>().as_ref() }
54
    }
55

            
56
    /// An empty handle (also what the non-`text_layout` build returns).
57
1
    #[must_use] pub const fn empty() -> Self {
58
1
        Self {
59
1
            ptr: core::ptr::null_mut(),
60
1
            run_destructor: false,
61
1
        }
62
1
    }
63
}
64

            
65
impl Clone for FontCacheSnapshot {
66
    fn clone(&self) -> Self {
67
        #[cfg(feature = "text_layout")]
68
        {
69
            if let Some(fm) = self.as_font_manager() {
70
                return Self::from_font_manager(fm.clone_shared());
71
            }
72
        }
73
        Self::empty()
74
    }
75
}
76

            
77
impl Drop for FontCacheSnapshot {
78
1
    fn drop(&mut self) {
79
1
        if self.run_destructor && !self.ptr.is_null() {
80
            #[cfg(feature = "text_layout")]
81
            unsafe {
82
                drop(Box::from_raw(self.ptr.cast::<InnerFontManager>()));
83
            }
84
            self.ptr = core::ptr::null_mut();
85
            self.run_destructor = false;
86
1
        }
87
1
    }
88
}
89

            
90
// SAFETY: the wrapped FontManager's shared state is Arc/Mutex-guarded; the
91
// handle exists precisely so print jobs can run off the UI thread.
92
#[cfg(feature = "text_layout")]
93
unsafe impl Send for FontCacheSnapshot {}
94

            
95
/// Boxed snapshot of the app's registered images (`css id -> ImageRef`).
96
///
97
/// Obtain via `CallbackInfo::get_image_cache_clone()`. The map is copied but
98
/// every `ImageRef` is a refcounted handle — decoded pixel data is shared,
99
/// never duplicated or re-decoded.
100
#[repr(C)]
101
#[derive(Debug)]
102
pub struct ImageCacheSnapshot {
103
    /// Boxed [`azul_core::resources::ImageCache`] (opaque over the ABI).
104
    pub ptr: *mut c_void,
105
    /// Standard azul destructor latch (see [`FontCacheSnapshot::run_destructor`]).
106
    pub run_destructor: bool,
107
}
108

            
109
impl ImageCacheSnapshot {
110
    /// Wrap an image cache into an ABI handle.
111
2
    #[must_use] pub fn from_image_cache(cache: azul_core::resources::ImageCache) -> Self {
112
2
        Self {
113
2
            ptr: Box::into_raw(Box::new(cache)).cast(),
114
2
            run_destructor: true,
115
2
        }
116
2
    }
117

            
118
    /// Borrow the wrapped image cache, if any.
119
5
    #[must_use] pub const fn as_image_cache(&self) -> Option<&azul_core::resources::ImageCache> {
120
        unsafe {
121
5
            self.ptr
122
5
                .cast::<azul_core::resources::ImageCache>()
123
5
                .as_ref()
124
        }
125
5
    }
126

            
127
    /// An empty handle.
128
1
    #[must_use] pub const fn empty() -> Self {
129
1
        Self {
130
1
            ptr: core::ptr::null_mut(),
131
1
            run_destructor: false,
132
1
        }
133
1
    }
134
}
135

            
136
impl Clone for ImageCacheSnapshot {
137
1
    fn clone(&self) -> Self {
138
        // `azul_core::resources::ImageCache` has no `Clone` impl (house rule
139
        // after the derive(Clone)+Drop double-free audit); clone the map of
140
        // refcounted `ImageRef` handles explicitly — pixels stay shared.
141
1
        self.as_image_cache().map_or_else(Self::empty, |c| {
142
1
            Self::from_image_cache(azul_core::resources::ImageCache {
143
1
                image_id_map: c.image_id_map.clone(),
144
1
            })
145
1
        })
146
1
    }
147
}
148

            
149
impl Drop for ImageCacheSnapshot {
150
3
    fn drop(&mut self) {
151
3
        if self.run_destructor && !self.ptr.is_null() {
152
2
            unsafe {
153
2
                drop(Box::from_raw(
154
2
                    self.ptr.cast::<azul_core::resources::ImageCache>(),
155
2
                ));
156
2
            }
157
2
            self.ptr = core::ptr::null_mut();
158
2
            self.run_destructor = false;
159
2
        }
160
3
    }
161
}
162

            
163
// SAFETY: ImageRef's refcount is atomic; the snapshot exists to outlive the
164
// callback (off-thread print jobs).
165
unsafe impl Send for ImageCacheSnapshot {}
166

            
167
/// Boxed pagination analysis (`page_breaks::PaginationInfo`) over the ABI.
168
///
169
/// The document-editor precalculation result: page count, page of any Y, and
170
/// every break position, WITHOUT any per-page display list having been
171
/// materialized. Obtain via `Pdf::compute_pagination`.
172
#[cfg(feature = "text_layout")]
173
#[repr(C)]
174
#[derive(Debug)]
175
pub struct PaginationSnapshot {
176
    /// Boxed [`crate::solver3::page_breaks::PaginationInfo`] (opaque over the ABI).
177
    pub ptr: *mut c_void,
178
    /// Standard azul destructor latch (see [`FontCacheSnapshot::run_destructor`]).
179
    pub run_destructor: bool,
180
}
181

            
182
#[cfg(feature = "text_layout")]
183
impl PaginationSnapshot {
184
    /// Wrap a pagination analysis into an ABI handle.
185
    #[must_use]
186
    pub fn from_info(info: crate::solver3::page_breaks::PaginationInfo) -> Self {
187
        Self {
188
            ptr: Box::into_raw(Box::new(info)).cast(),
189
            run_destructor: true,
190
        }
191
    }
192

            
193
    /// Borrow the wrapped analysis, if any.
194
    #[must_use]
195
    pub const fn as_info(&self) -> Option<&crate::solver3::page_breaks::PaginationInfo> {
196
        unsafe {
197
            self.ptr
198
                .cast::<crate::solver3::page_breaks::PaginationInfo>()
199
                .as_ref()
200
        }
201
    }
202

            
203
    /// An empty handle (0 pages — the failure value).
204
    #[must_use]
205
    pub const fn empty() -> Self {
206
        Self {
207
            ptr: core::ptr::null_mut(),
208
            run_destructor: false,
209
        }
210
    }
211

            
212
    /// Number of pages (0 for an empty/failed handle).
213
    #[must_use]
214
    pub fn page_count(&self) -> usize {
215
        self.as_info().map_or(0, |i| i.page_count)
216
    }
217

            
218
    /// Total document-space content height.
219
    #[must_use]
220
    pub fn total_content_height(&self) -> f32 {
221
        self.as_info().map_or(0.0, |i| i.total_content_height)
222
    }
223

            
224
    /// Number of page BREAKS (= `page_count - 1` for non-degenerate docs).
225
    #[must_use]
226
    pub fn break_count(&self) -> usize {
227
        self.as_info().map_or(0, |i| i.breaks.len())
228
    }
229

            
230
    /// Document-space Y of break `index` (0.0 out of range).
231
    #[must_use]
232
    pub fn break_y(&self, index: usize) -> f32 {
233
        self.as_info()
234
            .and_then(|i| i.breaks.get(index))
235
            .map_or(0.0, |b| b.y)
236
    }
237

            
238
    /// Whether break `index` was FORCED by CSS (`break-before/after`).
239
    #[must_use]
240
    pub fn break_is_forced(&self, index: usize) -> bool {
241
        self.as_info()
242
            .and_then(|i| i.breaks.get(index))
243
            .is_some_and(|b| {
244
                matches!(b.kind, crate::solver3::page_breaks::BreakKind::Forced)
245
            })
246
    }
247

            
248
    /// Whether break `index` was MOVED by an avoid-rule (break-inside /
249
    /// widows-orphans / atomic lines or rows).
250
    #[must_use]
251
    pub fn break_was_avoided(&self, index: usize) -> bool {
252
        self.as_info()
253
            .and_then(|i| i.breaks.get(index))
254
            .is_some_and(|b| {
255
                matches!(
256
                    b.kind,
257
                    crate::solver3::page_breaks::BreakKind::Avoided { .. }
258
                )
259
            })
260
    }
261

            
262
    /// Which page a document-space Y lands on ("what page is this node on?"
263
    /// — the editor query that needs NO page to be materialized).
264
    #[must_use]
265
    pub fn page_of_y(&self, y: f32) -> usize {
266
        self.as_info()
267
            .map_or(0, |i| crate::solver3::page_breaks::page_of_y(&i.breaks, y))
268
    }
269
}
270

            
271
#[cfg(feature = "text_layout")]
272
impl Clone for PaginationSnapshot {
273
    fn clone(&self) -> Self {
274
        self.as_info()
275
            .map_or_else(Self::empty, |i| Self::from_info(i.clone()))
276
    }
277
}
278

            
279
#[cfg(feature = "text_layout")]
280
impl Drop for PaginationSnapshot {
281
    fn drop(&mut self) {
282
        if self.run_destructor && !self.ptr.is_null() {
283
            unsafe {
284
                drop(Box::from_raw(
285
                    self.ptr
286
                        .cast::<crate::solver3::page_breaks::PaginationInfo>(),
287
                ));
288
            }
289
            self.ptr = core::ptr::null_mut();
290
            self.run_destructor = false;
291
        }
292
    }
293
}
294

            
295
// SAFETY: plain data (Ys + kinds), no interior mutability.
296
#[cfg(feature = "text_layout")]
297
unsafe impl Send for PaginationSnapshot {}
298

            
299
#[cfg(test)]
300
mod tests {
301
    use super::*;
302

            
303
    #[test]
304
1
    fn image_cache_handle_clone_shares_refs_and_drops_clean() {
305
1
        let inner = azul_core::resources::ImageCache::default();
306
1
        let handle = ImageCacheSnapshot::from_image_cache(inner);
307
1
        let clone = handle.clone();
308
1
        assert!(handle.as_image_cache().is_some());
309
1
        assert!(clone.as_image_cache().is_some());
310
1
        drop(handle);
311
1
        assert!(clone.as_image_cache().is_some());
312
1
    }
313

            
314
    #[test]
315
1
    fn empty_handles_are_null_and_droppable() {
316
1
        let f = FontCacheSnapshot::empty();
317
1
        let i = ImageCacheSnapshot::empty();
318
1
        assert!(i.as_image_cache().is_none());
319
1
        drop(f);
320
1
        drop(i);
321
1
    }
322
}